速度混合
计算速度混合量,速度在各个方向上的占比。
比如我们有前后左右四个方向的动作,角色向左前方向移动,那么四个动作融合后,左前两个动作的混合比重是要比右后大的。
void UCwlBaseAnimIns::CalculateVelocityBlend()
{
FVector VelocityNormal = UKismetMathLibrary::Normal(Velocity);
FVector LocalRelativeVelocityDir = UKismetMathLibrary::Quat_UnrotateVector(Character->GetActorRotation().Quaternion(), VelocityNormal);
float Sum = FMath::Abs(LocalRelativeVelocityDir.X) + FMath::Abs(LocalRelativeVelocityDir.Y) + FMath::Abs(LocalRelativeVelocityDir.Z);
LocalRelativeVelocityDir = LocalRelativeVelocityDir / Sum;
FCwlVelocityBlend TargetVelocityBlend;
TargetVelocityBlend.Forward = FMath::Clamp(LocalRelativeVelocityDir.X, 0.f, 1.f);
TargetVelocityBlend.Backward= FMath::Abs(FMath::Clamp(LocalRelativeVelocityDir.X, -1.f, 0.f));
TargetVelocityBlend.Left = FMath::Abs(FMath::Clamp(LocalRelativeVelocityDir.Y, -1.f, 0.f));
TargetVelocityBlend.Right = FMath::Clamp(LocalRelativeVelocityDir.Y, 0.f, 1.f);
float DeltaSeconds = GetWorld()->GetDeltaSeconds();
VelocityBlend = InterVelocityBlend(VelocityBlend, TargetVelocityBlend, VelocityBlendInterpSpeed, DeltaSeconds);
}
速度去掉旋转在单位化。原作者的写法。感觉应该先去掉旋转再单位化?
FVector VelocityNormal = UKismetMathLibrary::Normal(Velocity);
FVector LocalRelativeVelocityDir = UKismetMathLibrary::Quat_UnrotateVector(Character->GetActorRotation().Quaternion(), VelocityNormal);
然后每一项除以总值就变成了百分比。然后再限制一下范围再abs一下。
float Sum = FMath::Abs(LocalRelativeVelocityDir.X) + FMath::Abs(LocalRelativeVelocityDir.Y) + FMath::Abs(LocalRelativeVelocityDir.Z);
LocalRelativeVelocityDir = LocalRelativeVelocityDir / Sum;
这样还没完。我们是用速度分量驱动动画。但是当前的值和目标值直接的过度也需要处理一下。
VelocityBlend = InterVelocityBlend(VelocityBlend, TargetVelocityBlend, VelocityBlendInterpSpeed, DeltaSeconds);
其实就是每个分量根据配置的插值速度进行一个插值。
FCwlVelocityBlend UCwlBaseAnimIns::InterVelocityBlend(FCwlVelocityBlend Current, FCwlVelocityBlend Target, float InterSpeed, float DeltaX)
{
FCwlVelocityBlend NewVelocityBlend;
NewVelocityBlend.Forward = UKismetMathLibrary::FInterpTo(Current.Forward, Target.Forward, DeltaX, InterSpeed);
NewVelocityBlend.Backward = UKismetMathLibrary::FInterpTo(Current.Backward, Target.Backward, DeltaX, InterSpeed);
NewVelocityBlend.Left = UKismetMathLibrary::FInterpTo(Current.Left, Target.Left, DeltaX, InterSpeed);
NewVelocityBlend.Right = UKismetMathLibrary::FInterpTo(Current.Right, Target.Right, DeltaX, InterSpeed);
return NewVelocityBlend;
}
移动方向
通过角色方向与速度方向来计算移动方向是前后左右中的哪一个。
【这个笔记是以前写的,在看了Lyra后发现有一个 UKismetAnimationLibrary::CalculateDirection 接口可以用】
如果是冲刺,或者是旋转模式是查看方向,那么不论怎么样都是向前移动的。
否则,先根据速度方向和角色朝向的夹角计算出Angle。
在水平面上移动方向与angle的示意图如上。
我们根据如果angle在 AOB之间就认为是前方向。其他同理。同时这里允许右5度的误差。
void UCwlBaseAnimIns::CalculateMovementDirection()
{
if (Gait == ECwlGait::Sprinting)
{
MovementDirection = ECwlMovementDirection::Forward;
return;
}
if (RotationMode == ECwlRotationMode::LookingDirection || RotationMode == ECwlRotationMode::Aiming)
{
// UKismetMathLibrary::Conv_VectorToRotator(Velocity) 速度方向转到X轴正方向(对于摄像机这个是前)的旋转 减去 控制器的旋转
// 获得的这个angle角色正前方是0,右边半圈正180,左边半圈负的180
float Angle = UKismetMathLibrary::NormalizedDeltaRotator(UKismetMathLibrary::Conv_VectorToRotator(Velocity), AimingRotation).Yaw;
CalculateQudrant(70, -70, 110, -110, 5, Angle);
}
else if (RotationMode == ECwlRotationMode::VelocityDirection)
{
MovementDirection = ECwlMovementDirection::Forward;
}
}
void UCwlBaseAnimIns::CalculateQudrant(float Fr, float Fl, float Br, float Bl, float Buffer, float Angle)
{
bool bIncreaseBuffer = MovementDirection != ECwlMovementDirection::Forward || MovementDirection != ECwlMovementDirection::Backward;
if (AngleInRange(Angle, Fl, Fr, Buffer, bIncreaseBuffer))
{
MovementDirection = ECwlMovementDirection::Forward;
return;
}
bIncreaseBuffer = MovementDirection != ECwlMovementDirection::Right || MovementDirection != ECwlMovementDirection::Left;
if (AngleInRange(Angle, Fr, Br, Buffer, bIncreaseBuffer))
{
MovementDirection = ECwlMovementDirection::Right;
return;
}
bIncreaseBuffer = MovementDirection != ECwlMovementDirection::Right || MovementDirection != ECwlMovementDirection::Left;
if (AngleInRange(Angle, Bl, Fl, Buffer, bIncreaseBuffer))
{
MovementDirection = ECwlMovementDirection::Left;
return;
}
MovementDirection = ECwlMovementDirection::Backward;
}
bool UCwlBaseAnimIns::AngleInRange(float Angle, float MinRange, float MaxRange, float Buffer, bool bIncreaseBuffer)
{
if (bIncreaseBuffer)
{
return UKismetMathLibrary::InRange_FloatFloat(Angle, MinRange - Buffer, MaxRange + Buffer, true, true);
}
return UKismetMathLibrary::InRange_FloatFloat(Angle, MinRange + Buffer, MaxRange - Buffer, true, true);
}
八向移动状态机
输入姿势
我们在前面【移动步幅与走跑混合】得到一个混合空间
在【冲刺混合】章节获得了一个冲刺的Pose
我们通过步态来混合普通的前移动,和冲刺移动。保存为姿势 (N) F Movement
这只是Forward方向的输入。
但是由于作者walk,run动画都做了六个方向
所以我们有六个混合空间
那么我们就要处理,前,后,左前,左后,右前,右后六个移动状态。
除了前面的Forward。其他都只需要混合空间save pose
参数就是前文提到的,移动步幅,走跑混合,播放速率
同时注意这六个混合空间,六个冲刺动画要设置为同一同步组。
不然混合会出问题,我这里的表现为罗圈腿。查了很久。。。
把所有逻辑写在一个动画层中 (N)CycleBlending。这里包括了六个混合空间保持为变量。和一个状态机。
状态机里面是前,后,左前,左后,右前,右后。六个运动状态
输出状态
在每一个状态中都是类似的结构。根据前后左右四个方向速度的占比,决定四个方向动画的混合度。
目的是,你也许已经切换到了左前的状态机中,但是不能直接切到单个左前动画。有一定的过度过程。
如果这里只连一个左前动画,且动画从左后过度过来。那么效果就是动画 LB 过度到 动画LF。过度时间是这个条件的混合设置。
上面是LF,下面是LB
猜测这种切换会有问题,实际测试效果还是有明显差异的。
过度条件
我粘了一份状态机,去除了某些花里胡哨的切换条件,描述一下状态切换条件。
我们在【移动方向】章节算了一个移动方向 MovementDirection。
UENUM(BlueprintType)
enum class ECwlMovementDirection : uint8
{
Forward,
Right,
Left,
Backward
};
比如向【后】变化的状态,切换条件都是 MovementDirection == Backward。图中我描粗的绿色
切换条件是 MovementDirection == Right,图中的紫色。
===
外面一圈的混合设置
中间一圈的状态切换混合设置调整
中间一圈提升为共享Pivot。插值0.5,方式立方。
同时触发一个pivot事件,这个事件会设置一个bool值true。0.5秒后false。用于回转运动。回转运动指向左运动时候突然向右,有一个过度动作。
这里的change direction。打开骨骼,这里有一个混合配置。意图是转身的时候,上半身比下半身慢半拍
可以使用slomo 0.5 感受下效果
然后我们看一个问题,左移动,胸朝左,按右,胸还是朝左,需要前一下才能正确。
因为我们是对角切换,左前没法之间过来。
右前只能切到左后,而没有左前,需要添加左后到左前的过度条件。
共享混合设置
左边的条件如下,右边那个需要把 state weight 改成RB
也就是当完全到达左后状态,且满足曲线值为0,就可以自动切换到左前。曲线的含义是==0,那么步伐已经到了下半段
在walk LB, RB, LF, RF。run LB RB。动画中有这条Feet_Crossing曲线
分界点是脚步交叉,后面值为0
然后这还不完全
作者还加了一个臀部朝向的偏移值。这条曲线目前也是没有的,在覆盖层拉弓的侧身动作才会有这条曲线,所以现在这个一直是真。
过度提升为共享,内部都是Hips_Right,内部都是Hips_Left。混合设置都是switch_hips
然后条件如上
而且这四个条件的,优先顺序都是2
最后添加一些事件,后面备用
在动画蓝图中触发事件调到cpp层
void UCwlBaseAnimIns::OnAnimNotifyHipsChange(ECwlHipsDirection InHipsDirection)
{
HipsDirection = InHipsDirection;
}
其他选择
以上是ALSv4的做法。还可以选择Lyra的做法。这个是Lyra的状态机。我们只看Cycle状态
截图来自我一个抄Lyra的项目[https://gitee.com/weilin3101/my-lyra-v1]
Cycle状态中是一个动画层
动画层中
UpdateCycleAnim的一部分
Sequence Player 通过 UpdateCycle函数,通过读取运动方向,从前后左右四个动画中选了一个,作为当前动画。然后调用朝向扭曲节点,实现了八项移动。需要包含 Animation Locomotion Library插件。
但是效果上我更喜欢ALSv4的版本