[UE5]完全程序化的角色基础移动控制器

程序化Locomoition角色控制器

git仓库:GitHub - EanoJiang/ProceduralAnim: 完全程序化Locomotion · GitHub

视频演示:【UE5】完全程序化的角色基础移动控制器_哔哩哔哩_bilibili

Debug调试显示:

1774600728384

第一个程序动画

创建一个control rig

1771432079406

control rig蓝图中,获取骨骼的transform,暂时先控制pelvis来验证该功能,选择Space为全局空间,让过渡Translation的Z轴附加一个sin函数,模拟身体上下浮动

1771432273355

选择Use Specific Animation

1771432468447

效果:

1771432739182

BasicLegIK

基础的腿部IK

设置好腿部IK链:thigh-calf-foot,Efector设置为ik_foot

1771696491553

这里还需要设置主次朝向,也就是Primary和Secondaru Axis

Primary Axis为Item A(也就是thigh),Secondaru Axis为Item B(也就是calf)

thigh_l的坐标系如图(红绿蓝分别对应xyz):

1771696810920

calf_l的坐标系如图:

1771696918143

大腿需要朝向下,膝盖需要朝向正前

因此Primary Axis的x=-1,Secondaru Axis的y=1

1771697607866

Pole Vector(极向量) :控制 IK 的关节弯曲方向

因此设置朝向正前

1771697799806

效果:

1771698439534

把控制脚部偏移的节点加进来

1771699821664

用循环来为左右脚都设置IK

先建立一个脚部Transform的列表

1771713374328

然后给左右脚都设置好ik,因为右脚的骨骼朝向与左脚相反,所以当index!=0时主次轴朝向都需要乘-1

1771713587578

效果:

1771715122305

暂时先删去脚部偏移

1771721536395

FootRotation

脚的旋转

RotateAroundPoint

脚绕着目标点旋转

也就是“要旋转的物体” 相对于 “旋转中心点” 的旋转偏移,如图所示也就是向量A-B

1772088881553

1772004217010

四元数乘的顺序问题:

A*B 先乘B后乘A

也就是Quat_B要乘以Quat_A的量,因此要旋转的Transform放在B

RotateVector 节点:

Transform:旋转信息变换,也就是旋转量

Vector:要被旋转的方向向量Vector

SetFootTransforms

根据脚的位置计算脚的旋转方式

如果脚在身体前面,脚绕着脚踝旋转

如果脚在身体后面,脚绕着脚前掌旋转

Foot以及Ball骨骼的引用

1771923895200

1771923812800

脚踩平台追踪

从每个Foot的z轴(-50,50)追踪脚踩平台的接触点

1771926458334

1771926161002

脚部放置

设置Foot的Transform属性,Translation(位置)取自脚踩平台高度,Rotation和Scale保持Foot本身的不变

1771927780254

效果:

1771928419031

脚踩平台旋转偏移

最终脚部的Rotation = 脚踩平台的Rotation * 脚部的Rotation

1772006712916

再次强调四元数乘的顺序问题:

A * B 先 B,后 A 子对象(A)在父对象(B)的空间内变换
B * A 先 A,后 B 父对象(B)在子对象(A)的空间内变换

A是子对象,B是父对象

我们需要的是先让脚部按照自身的 Foot.Rotation 旋转,然后再将整个脚部(包括它自己的旋转)作为一个整体,跟随平台的 TargetFootPlatform.Rotation 进行旋转

也就是平台先转,脚再跟着平台转,因此是脚踩平台的 TargetFootPlatform.Rotation* Foot.Rotation,而不是反过来乘

先绕Z轴旋转45度看下效果:

1772003050920

1772006792679

计算脚踩平台的前后偏移量

向前方向是y = 1

1772013041788

如果脚在后面,那么点积结果<0,因此就可以根据这两个向量的点积结果来判断脚的位置(相对于脚踩处的位置)

1772014163983

因此,(Vector_脚踩平台位置 - Vector_大腿位置) ·(0,1,0)得到的值即为脚部向前偏移量FootForwardOffset

1772013837859

绕着脚踩平台旋转脚部

删去之前写的Rotation逻辑,直接取用FootRig.Rotation即可

然后再单独处理脚部旋转逻辑:调用之前写的RotateAroundPoint函数,待旋转的是脚部,旋转点和旋转量是脚踩处

1772015965153

计算脚前掌ball的旋转偏移点

Make Relative :将一个变换从全局空间转换为相对于另一个变换的局部空间

全局坐标中,脚前掌旋转点的水平方向XY取自脚前掌,高度Z取自脚踩处

需要变换到脚部Foot的局部坐标

1772090580544

在脚部旋转的逻辑处理之后,可视化脚前掌旋转点

Transform相乘:坐标系转换,通常是子Transform乘以父Transform转换为世界坐标

1772090515152

如图中绿色框

1772075837766

如果脚部的z轴旋转量增大,会发现脚前掌旋转点位置不精确,这是因为最终的LegIK会限制脚部的旋转

计算脚尖tip的旋转偏移点

ball沿着脚底向前移动一段距离即为tip

脚底向前方向向量 = Vector_脚前掌(ball)- Vector_脚踝(foot)

1772089335200

脚底向前方向向量 + 脚前掌Ball的世界坐标 = 脚尖旋转偏移点的世界坐标,然后相关于脚前掌的局部坐标

1772090864997

绘制脚尖旋转偏移点(需要转换为世界坐标)

1772177837011

最终得到

1772089382802

计算脚后跟heel的旋转偏移点

脚踩点位置 + 脚底向前方向向量*(-0.8) = 脚后跟旋转偏移点的世界坐标,然后相关于脚部的局部坐标

1772092948348

绘制脚后跟旋转偏移点

1772177870868

最终得到

1772093237126

所有点位

1772093487161

封装绘制这些点位的函数

1772178342219

1772178441775

旋转脚部

腿在后面:先绕着脚前掌Ball旋转脚部Foot,再绕着脚尖Tip旋转脚部Foot

因为旋转量是取自脚部向前偏移量,因此把这个偏移量分为两个阶段:(-10 ~ -40),(-40 ~ -70)

绕着脚前掌Ball旋转脚部Foot

旋转量取自脚部向前偏移量,从(-40,-10)Remap为(-25,0)

1772182237348

暂时关闭SetFinalLegIK

1772179113059

设置脚部前后偏移(沿y轴)

1772179144232

1772178852073

修改一下前面计算点位时的Z轴逻辑

脚前掌

1772178658418

脚后跟

1772178769163

取消旋转脚前掌ball

1772180893796

绕着脚尖Tip旋转脚部Foot

旋转量取自脚部向前偏移量,从(-70,-40)Remap为(-30,0)

1772180915709

腿在前面:绕着脚后跟heel旋转脚部Foot

1772181245196

开启SetFinalLegIK

1772181797229

设置Z轴偏移,防止脚部浮空

1772181765909

最终效果:

1772182929911

Velocity cycles and leg movement

计算速度

CalculateVelocity

世界空间速度 = (根骨骼世界坐标 - 上一帧的根骨骼世界坐标) / DeltaTime

1772250962641

骨骼空间速度 = (世界空间速度 + 骨骼位置).转换到骨骼空间 - 骨骼位置

绘制相对于根骨骼的角色速度 = 从根骨骼位置出发 -> (根骨骼位置+骨骼空间速度)

1772250990422

1772251672715

锁定脚部位置的数组

构造一个LockedFootLocationArray

1772272837034

1772263836582

根骨骼前后帧的相对变换:T_now * T_last⁻¹:相当于 “先撤销上一帧的所有变换(逆操作),再执行当前帧的变换”,最终得到的就是 “从上一帧到当前帧,物体相对动了多少”

1772262856028

计算Cycle

1772264943770

1772264926816

计算脚踩目标平台的位置

CalculateFootTargetPlatform

1772272870902

1772272891048

UnLocked状态

对脚踩目标平台位置进行Lerp线性插值计算

如果处于UnLocked状态,让局部变量脚踩目标平台位置(TempFootPlatform)从 原锁定位置-Lerp->新的脚部位置,Lerp的阻尼取自Cycle进度,也就是Cycle从0 ~ 0.5映射到Lerp的阻尼就是0 ~ 1

为什么是0.5?因为下面会设置规则Cycle百分比大于0.5就锁定脚部

1772273005885

更新数组

然后更新脚部锁定数组Cycle百分比大于0.5就锁定,再更新脚部锁定位置数组

1773802799817

Locked状态

1772391588977

UnLocked状态,对未来脚部位置进行预测,然后Lerp

之前只是默认Lerp到角色正下方的脚部位置,现在需要预测角色即将到达的脚部位置,再Lerp

新建预测脚部位置数组

1772433873869

预测脚部落点

1772435916646

适配步幅的骨骼空间移动速度 = 骨骼空间速度的单位向量 × (步幅/2)

1772436641931

得到的PredictFeetLocationArray就可以传入CalculateFootTargerTransform函数,将脚踩目标平台Lerp到角色未来即将到达的脚部位置

1772435938098

1772435965012

效果:

1772436971225

交替移动两条腿

脚部Cycle以异步模式进行

新建每只脚的Cycle百分比数组

1772437991266

每只脚的Cycle百分比 = 主Cycle百分比 + (FootIndex × 0.5) ,最后保证这个百分比超过1自动重置

这样当开始主Cycle时,左脚index = 0,右脚index = 1,右脚就比左脚慢了半个周期

1772440215913

替换计算目标脚踩平台中的MasterCyclePercent为每只脚的Cycle百分比

1772440713807

1772440685761

确保脚底在地面上

1772442061844

预测角色的移动以实现脚部落点的追踪

未落地前的的骨骼空间移动速度应该是即将前进的距离,加上移动方向上的步幅,这样在脚部遇到障碍物之前,会提前抬高一些防止脚部穿模

其中,即将前进的距离 = RigSpaceVelocity × 慢慢变小的预测时间,由于预测时间在慢慢减小,这个前进的距离也在由大变小,因此脚部抬高与前进会产生一个曲线,自然过渡到可能要遇到的障碍物的平面落点

1772457150788

计算脚部的插值平滑曲线

位置Translation从StartingTransform平滑插值到EndTransform,平滑模式设置为BSpline(全局平滑),中间可自定义2个插值的z轴高度

1772461785003

旋转Rotation直接简单Lerp插值即可

1772461821241

回到计算脚踩目标平台的函数:将原来的Interpolate替换为写好的平滑曲线过渡

1772462148199

效果:

1772462372749

动态Cycle时间

1772462557496

可以发现,如果移动速度过快,会出现往后迈的腿的锁定位置远远落后,导致该腿会绷直一段时间,很不自然

这是因为:主Cycle百分比是固定值,当速度很快的时候脚的锁定时间过长

因此需要改为动态的Cycle时间,当移动速度变得更快的时候这个Cycle时间要相应的更短

1772467125971

得到的摆动时间占Cycle周期长度的百分比SwingTimeAsAPercent替换掉原来的固定值0.5

1772467268936

1772467306421

1772467705734

修复脚部锁定过于向前:抵消掉预测即将前进距离

1772467442570

修复:静止状态时原地踏步的问题

1772468125823

不同速度下,应该对应的平滑曲线示意图:

高速——黄色关键帧和红色曲线所示,低速——蓝色关键帧和紫色曲线所示

1772469476856

改造CalculateFootSpline

1773803691171

从起始点沿着移动速度方向,向前/后偏移一定距离处,-1后退,1前进

因此点位1就是开始位置0向后偏移8,点位2是结束位置6向前偏移20——(偏移量随时更改)

1772523482254

点位2是点位1沿z轴向上偏移30,点位4是点位5沿z轴向上偏移30,点位3是2和4的中点——(偏移量随时更改)

1773803743394

最后按序传入BSpline平滑曲线的点位,得到的Position传给返回值OutputTransform的Translation属性,Rotation属性这里先直接用StartingTransform和EndTransform的混合值即可

1773803835302

效果:

1773804198631

适当减小地面停留时间,让脚部重置的响应快一些

1772524308916

修复:脚部落地前出现一段时间的大小腿绷直

这是因为脚部的目标IK位置距离太远,脚部要想到达该位置会尽量伸直大小腿来实现,膝盖就会绷得很紧

解决方法:限制IK距离

1772527890218

Foot到Calf + Calf到Thigh,距离之和是大小腿的总长,那么脚部IK的目标位置不能超过腿长的1.02——(这个值可以调整)

要保证获取的Transform是初始的值,不要被其他地方修改所影响,因此要勾选Initial

1772527932189

效果:

1772528278547

Pelvis and spine control

身体控制

初始化设置——盆骨偏移OffsetPelvis放到SetFinalIK之前

1772534591440

先保存OffsetPelvis之前的脚部Transform,然后恢复

1772533122178

1772531682587

Pelvis Cycle

基于速度的身体上下循环偏移

1772607757245

身体左右旋转循环偏移

人体运动学:左脚迈出时,上半身体顺时针旋转;右脚迈出时,上半身体逆时针旋转

1772614623284

因此,需要比较哪个脚在前,来决定上半身体的旋转方向以及旋转角度

1772616847472

1772616889142

效果:

1772619583824

肩膀左右旋转循环偏移

用来补偿身体的左右旋转带来的手臂顺拐现象,因此肩膀的左右旋转方向与身体应当是相反的

SpineRotation = PelvisRotation × -2

1772618961015

1772680505185

1772680516455

效果:

1772619740146

如果把Scale设置的更大,可以看到更夸张的效果,因此这个Scale可以根据想要的效果更改值大小:

1772619402343

脖子左右旋转循环偏移

1772680416619

1772680549327

效果:

1772680758792

斜向移动时骨骼朝向偏移

将脚踩平台的Transform存为数组以供随时访问

在SetupFootArray中初始化后,再存储该数组

1772693516310

用左右脚踩平台用来替换左右脚的引用

1772695548617

斜向移动时左右脚的朝向偏移

对于靶向移动来说,在同一条对角线上的两个方向移动时的脚朝向是相同的,因此只需要考虑角色面前的180度半圆内的脚朝向

需要设置一定死区,防止越过临界值的时候移动方向突变,这里设置为10度死区

1772696872362

从图中可以看出:

向斜后方移动的角度 > 100,实际的脚部朝向角 = 移动方向角 - 180

向斜后方移动的角度 <-100,实际的脚部朝向角 = 移动方向角 + 180

1772703106545

得到的FootTargetZAngle转换为Quat,得到移动角度偏移MovementAngleOffset

1772765702434

逻辑代码迁移到C++自定义ControlRig节点:

需要.Build中添加编译要用到的模块AnimationCore

1772703564402

RigUnit_ProceduralCharacter.h

#pragma region 计算移动角度偏移
	//计算移动角度偏移
	USTRUCT(meta = (DisplayName = "GetMovementAngleOffset"))
	struct PROCEDURALANIM_API FRigUnit_GetMovementAngleOffset : public FRigUnit
	{
		GENERATED_BODY()

		RIGVM_METHOD()
		virtual void Execute() override;

		UPROPERTY(meta = (Input))
		FVector RigSpaceVelocity;

		UPROPERTY(meta = (Output))
		float FootTargetZAngle;

		UPROPERTY(meta = (Output))
		FQuat MovementAngleOffset;
	};

	FVector EulerFromQuat(const FQuat& Rotation, EEulerRotationOrder RotationOrder = EEulerRotationOrder::ZYX, bool bUseUEHandyness = false);

	FQuat FromTwoVectors(const FVector& A, const FVector& B);

#pragma endregion

RigUnit_ProceduralCharacter.cpp

#pragma region 计算移动角度偏移
	FRigUnit_GetMovementAngleOffset_Execute()
	{
		const float OriginalZAngle = AnimationCore::EulerFromQuat(
			FromTwoVectors(FVector(0,1,0),RigSpaceVelocity)
			).Z;
		if (OriginalZAngle > 100)
		{
			FootTargetZAngle = OriginalZAngle - 180;
		}
		else if (OriginalZAngle < -100)
		{
			FootTargetZAngle = OriginalZAngle + 180;
		}
		else
		{
			FootTargetZAngle = OriginalZAngle;
		}
		MovementAngleOffset = AnimationCore::QuatFromEuler(FVector(0,0,FootTargetZAngle));
	}

	FQuat FromTwoVectors(const FVector& A, const FVector& B)
	{
		if (A.IsNearlyZero() || B.IsNearlyZero())
		{
			return FQuat::Identity;
		}
		return FRigVMMathLibrary::FindQuatBetweenVectors(A, B);
	}
#pragma endregion

1772766089385

在PredictFootLandingSpot中传入移动角度偏移

1772767143284

效果:

1772768332916

在FinalLegIK中传递移动角度偏移到膝盖

1772779918985

效果:

1772780260626

身体跟随脚部的旋转而自旋转

上面解决了脚部旋转以及膝盖的跟随旋转,但是Pelvis还没有跟着旋转,因此需要找到双脚间的平均旋转偏移来附加到Pelvis上

1772788823676

1772786694356

预测脚步落点位置需要绕着身体旋转

1772789901280

效果:

八向移动

1772790080868

斜向移动

1772790290564

Smoothing and rotation limits

转向限制,尽可能避免出现腿部交叉的情况

减少方向切换时的移动角度偏移

1773000780089

让更新前后的向量平滑过渡(不受帧速率影响)

之前的向量更新是瞬时的,因此会出现腿部位置突变的情况,极其破坏动作的说服力

新建用于向量的Lerp函数(消除帧率差异)

LerpedVector = InVector + 差值 * 基于DeltaTime的变化量

1773026607721

迁移到C++

#pragma region 消除帧率差异的用于Vector的Lerp函数
	USTRUCT(meta = (DisplayName = "VectorLerp"), Category = "Lerp")
	struct PROCEDURALANIM_API FRigUnit_VectorLerpIndependentOnFrameRate : public FRigUnit
	{
		GENERATED_BODY()

		FRigUnit_VectorLerpIndependentOnFrameRate()
		{
			LerpedVector = TargetVector = InVector = FVector(1.f, 0.f, 0.f);
			MaxDelVectorPerSecond = 0.f;
		}
		RIGVM_METHOD()
		virtual void Execute() override;

		UPROPERTY(meta = (Input))
		FVector InVector;

		UPROPERTY(meta = (Input))
		FVector TargetVector;

		UPROPERTY(meta = (Input))
		float MaxDelVectorPerSecond = 0;

		UPROPERTY(meta = (Output))
		FVector LerpedVector;
	};

FVector MathVectorClampLength(FVector Value = FVector(1.f, 0.f, 0.f), float MinimumLength = 0, float MaximumLength = 1);
#pragma endregion
#pragma region 消除帧率差异的用于Vector的Lerp函数
FRigUnit_VectorLerpIndependentOnFrameRate_Execute()
{
	FVector DeltaVector = MathVectorClampLength(TargetVector - InVector, 0,MaxDelVectorPerSecond * ExecuteContext.GetDeltaTime<float>());
	LerpedVector = InVector + DeltaVector;
}

FVector MathVectorClampLength(FVector Value, float MinimumLength, float MaximumLength)
{
	if (Value.IsNearlyZero())
	{
		return FVector::ZeroVector;
	}
	float Length = static_cast<float>(Value.Size());
	return Value * FMath::Clamp<float>(Length, MinimumLength, MaximumLength) / Length;
}
#pragma endregion

RigSpaceVelocity加入自定义的VectorLerp节点

1773027985876

修复:预测脚部落点位置更新不及时的Bug

当阻尼值设置较小时会出现,停下后脚部落点位置更新不及时

1773028171213

这是因为停下的瞬间RigSpaceVelocity已经为0,这时候的移动方向是未知方向,因此移动方向上的步幅会出问题

因此还需要修改预测脚部落点位置的函数逻辑

移动方向上的步幅中,步幅原来的逻辑是RigSpaceVelocity的单位向量 × 步长的一半,因为归一化节点unit对于零向量会出问题,因此改为RigSpaceVelocity×在地面停留时间的一半

1773032574271

移动角度偏移MovementAngleOffset加入自定义的VectorLerp节点

考虑到之前把移动角度偏移MovementAngleOffset的逻辑也封装到了C++中,那么要想在C++中也调用这个自定义的VecotrLerp节点,需要把该节点的功能再封装一层为独立的函数

#pragma region 消除帧率差异的用于Vector的Lerp函数
	USTRUCT(meta = (DisplayName = "VectorLerp"), Category = "Lerp")
	struct PROCEDURALANIM_API FRigUnit_VectorLerpIndependentOnFrameRate : public FRigUnit
	{
		GENERATED_BODY()

		FRigUnit_VectorLerpIndependentOnFrameRate()
		{
			LerpedVector = TargetVector = InVector = FVector(1.f, 0.f, 0.f);
			MaxDelVectorPerSecond = 0.f;
		}
		RIGVM_METHOD()
		virtual void Execute() override;

		UPROPERTY(meta = (Input))
		FVector InVector;

		UPROPERTY(meta = (Input))
		FVector TargetVector;

		UPROPERTY(meta = (Input))
		float MaxDelVectorPerSecond = 0;

		UPROPERTY(meta = (Output))
		FVector LerpedVector;
	};

	FVector VectorLerpIndependentOnFrameRate(FVector InVector, FVector TargetVector, float MaxDelVectorPerSecond = 0, float DeltaTime = 0);

	FVector MathVectorClampLength(FVector Value = FVector(1.f, 0.f, 0.f), float MinimumLength = 0, float MaximumLength = 1);
#pragma endregion
#pragma region 消除帧率差异的用于Vector的Lerp函数
FRigUnit_VectorLerpIndependentOnFrameRate_Execute()
{
	float DeltaTime = ExecuteContext.GetDeltaTime<float>();
	LerpedVector = VectorLerpIndependentOnFrameRate(InVector, TargetVector, MaxDelVectorPerSecond, DeltaTime);
}

FVector VectorLerpIndependentOnFrameRate(FVector InVector, FVector TargetVector, float MaxDelVectorPerSecond, float DeltaTime)
{
	FVector DeltaVector = MathVectorClampLength(TargetVector - InVector, 0,MaxDelVectorPerSecond * DeltaTime);
	return InVector + DeltaVector;
}

FVector MathVectorClampLength(FVector Value, float MinimumLength, float MaximumLength)
{
	if (Value.IsNearlyZero())
	{
		return FVector::ZeroVector;
	}
	float Length = static_cast<float>(Value.Size());
	return Value * FMath::Clamp<float>(Length, MinimumLength, MaximumLength) / Length;
}
#pragma endregion

加入自定义的VectorLerp节点

1773034466179

迁移到C++:

		UPROPERTY(meta = (Input))
		float MaxDelVectorPerSecond = 360.0f;

1773035661355

		FVector LerpedVector = VectorLerpIndependentOnFrameRate(
			AnimationCore::EulerFromQuat(MovementAngleOffset),
			FVector(0,0,FootTargetZAngle),
			MaxDelVectorPerSecond,
			ExecuteContext.GetDeltaTime<float>()
			);

		MovementAngleOffset = AnimationCore::QuatFromEuler(FVector(0,0,LerpedVector.Z));

1773035703037

1773035609623

暂时把每秒的最大变换量设置为360,在这里也就是1圈

效果:

1773036272151

突然改变移动方向不会出现盆骨Rotation突变的情况

修复:侧向移动时脚部是平移过去的,没有正确旋转

正确次序是:绕脚掌ball旋转->绕脚尖tip旋转->绕脚后跟旋转->绕脚踩处旋转

原来的逻辑:

1773037347045

1773037443379

修改后:

1773037161646

1773037310392

限制脚部的旋转偏移

脚部的Z轴旋转(左右旋转)受移动角度偏移限制,限制权重为0.5

1773043107974

限制在(-25,25)度

1773042936582

将之前预测落点函数中的移动角度偏移权重也更换为这个变量

1773043047250

修复:当脚部经过身体正下方会处于很平的浮空

让脚部在完成Swing阶段后保持最初始的旋转信息

怎么判断已完成Swing阶段?

每只脚的Cycle实时百分比 / Swing阶段占比 < 1,说明当前处于Swing阶段

并且用曲线来平滑处理,中点就是Swing阶段时脚部经过身体正下方的时刻,权重为1,完全是最初始的旋转信息

1773049567322

效果:

1773049054575

修复:出现腿部交叉的情况

对比现实中,如果腿部即将交叉,我们会让一条腿绕着另一条腿向前或向后旋转

怎么实现"一条腿绕着另一条腿旋转"?

这个自定义脚部曲线的中间点3的水平面位置xy绕着盆骨旋转,旋转量为移动角度偏移,z轴位置还是原先的InsertPoint3

1773087349065

让步幅与移动速度相关

跑的时候步幅小,走的时候步幅大

1773092931518

Arm motion

手部运动

由于身体偏移是在z轴进行的,如果要让手部运动sin型,只需在身体偏移之前让手部运动沿着移动速度的方向做前后运动即可

建立HandArray

1773134402936

迁移到C++

#pragma region 初始化Array
USTRUCT(meta = (DisplayName = "SetupArray"))
struct PROCEDURALANIM_API FRigUnit_SetupArray : public FRigUnit_DynamicHierarchyBaseMutable
{
	GENERATED_BODY()

	RIGVM_METHOD()
	virtual void Execute() override;

	UPROPERTY(meta=(Output))
	TArray<FRigElementKey> FootArray;

	UPROPERTY(meta=(Output))
	TArray<FTransform> LockedFootLocationArray;

	UPROPERTY(meta=(Output))
	TArray<bool> IsFootLockedArray;

	UPROPERTY(meta=(Output))
	TArray<FTransform> PredictFeetLocationArray;

	UPROPERTY(meta=(Output))
	TArray<float> PerFootCyclePercentArray;

	UPROPERTY(meta=(Output))
	TArray<FTransform> SavedFootPlatformArray;

	UPROPERTY(meta=(Output))
	TArray<FRigElementKey> HandArray;
};
#pragma endregion
#pragma region SetupArray
FRigUnit_SetupArray_Execute()
{
	DECLARE_SCOPE_HIERARCHICAL_COUNTER_RIGUNIT()

	URigHierarchy* Hierarchy = ExecuteContext.Hierarchy;

	if(!Hierarchy)
	{
		return;
	}

	FootArray.Reset();
	LockedFootLocationArray.Reset();
	IsFootLockedArray.Reset();
	PredictFeetLocationArray.Reset();
	PerFootCyclePercentArray.Reset();
	SavedFootPlatformArray.Reset();
	HandArray.Reset();

	const FRigElementKey RootBoneKey(TEXT("root"), ERigElementType::Bone);
	if (!Hierarchy->Contains(RootBoneKey))
	{
		return;
	}

	for (const FRigElementKey& ChildKey : Hierarchy->GetChildren(RootBoneKey, true))
	{
		if (ChildKey.Type != ERigElementType::Bone)
		{
			continue;
		}
		const FString BoneNameStr = ChildKey.Name.ToString();

		if (BoneNameStr.Contains(TEXT("foot"), ESearchCase::IgnoreCase) && !BoneNameStr.Contains(TEXT("ik"), ESearchCase::IgnoreCase))
		{
			FootArray.Add(ChildKey);

			FVector LockedFootLocationElementTranslation = Hierarchy->GetGlobalTransform(ChildKey).GetTranslation() + FVector(0.0f, 0.0f, -13.5f);
			FTransform LockedFootLocationElement;
			LockedFootLocationElement.SetTranslation(LockedFootLocationElementTranslation);
			LockedFootLocationArray.Add(LockedFootLocationElement);

			IsFootLockedArray.Add(false);

			PredictFeetLocationArray.Add(FTransform());

			PerFootCyclePercentArray.Add(0);

			SavedFootPlatformArray.Add(FTransform());
		}
		else if (BoneNameStr.Contains(TEXT("hand"), ESearchCase::IgnoreCase) && !BoneNameStr.Contains(TEXT("ik"), ESearchCase::IgnoreCase))
		{
			HandArray.Add(ChildKey);
		}
	}
}
#pragma endregion

ArmMotion

1773136230299

1773136246710

主次朝向逻辑判断迁移到C++中:

#pragma region 计算ArmMotion的主次轴朝向数据
USTRUCT(meta = (DisplayName = "GetArmMotionAxisData"), Category = "ArmMotion")
struct PROCEDURALANIM_API FRigUnit_GetArmMotionAxisData : public FRigUnit
{
	GENERATED_BODY()

	RIGVM_METHOD()
	virtual void Execute() override;

	UPROPERTY(meta = (Input))
	int ArmIndex = 0;

	UPROPERTY(meta = (Output))
	FVector PrimaryAxis = FVector(1, 0, 0) ;

	UPROPERTY(meta = (Output))
	FVector SecondaryAxis = FVector(0, -1, 0);
};
#pragma endregion
#pragma region 计算ArmMotion的主次轴朝向数据
FRigUnit_GetArmMotionAxisData_Execute()
{
	//右骨骼朝向是反的,因此Index不为0时需要反向
	PrimaryAxis   = (ArmIndex == 0) ? FVector(1, 0, 0) : FVector(-1, 0, 0);
	SecondaryAxis = (ArmIndex == 0) ? FVector(0, -1, 0) : FVector(0, 1, 0);
}
#pragma endregion

1773136397589

给Effector加一点偏移,效果如下:

1773137363097

绕着肩膀旋转手臂

1773200218511

基于移动速度的Arm摆动旋转曲线:
RigSpaceVelocity.Length.Remap * sin( 2Π * (PerFootCyclePercent+0.25)%1 )

手臂摆动旋转曲线中,由于需要和腿部实际摆动周期同步,需要加上一定偏移量,这里暂时设为提前1/4周期

1773200263309

Arm摆动旋转曲线迁移到C++

#pragma region 计算ArmMotion的Effector的RotationAmount值
	//计算ArmMotion的Effector的RotationAmount值
	USTRUCT(meta = (DisplayName = "GetArmMotionEffectorRotationAmount"), Category = "ArmMotion")
	struct PROCEDURALANIM_API FRigUnit_GetArmMotionEffectorRotationAmount : public FRigUnit
	{
		GENERATED_BODY()

		RIGVM_METHOD()
		virtual void Execute() override;

		UPROPERTY(meta = (Input))
		TArray<float> PerFootCyclePercentArray;

		UPROPERTY(meta = (Input))
		int ArmIndex;

		UPROPERTY(meta = (Input))
		FVector RigSpaceVelocity;

		UPROPERTY(meta = (Output))
		FQuat RotateAmount;
	};

	float MathFloatRemap(float Value, float SourceMinimum, float SourceMaximum, float TargetMinimum, float TargetMaximum, bool bClamp);

#pragma endregion
#pragma region 计算ArmMotion的Effector的RotationAmount值
FRigUnit_GetArmMotionEffectorRotationAmount_Execute()
{
	const float PerFootCyclePercent = PerFootCyclePercentArray[ArmIndex];
	//RigSpaceVelocity.Length.Remap * sin( 2Π * (PerFootCyclePercent+0.25)%1 )
	const float ArmSwingCurve = sin(2 * UE_PI * FMath::Fmod(PerFootCyclePercent + 0.25f, 1.0f))
						* MathFloatRemap(
							RigSpaceVelocity.Length(),
							0,
							500,
							0,
							30,
							true
							);

	RotateAmount = AnimationCore::QuatFromEuler(FVector(ArmSwingCurve, 0, 0));
}

float MathFloatRemap(float Value, float SourceMinimum, float SourceMaximum, float TargetMinimum, float TargetMaximum, bool bClamp)
{
	float Result = 0.f;
	float Ratio = 0.f;
	if (FMath::IsNearlyEqual(SourceMinimum, SourceMaximum))
	{
		Ratio = 0.f;
	}
	else
	{
		Ratio = (Value - SourceMinimum) / (SourceMaximum - SourceMinimum);
	}
	if (bClamp)
	{
		Ratio = FMath::Clamp<float>(Ratio, 0.f, 1.f);
	}
	Result = FMath::Lerp<float>(TargetMinimum, TargetMaximum, Ratio);
	return Result;
}
#pragma endregion

迁移后:

1773200490788

效果:

1773200606409

给Hand加一个基于移动速度的Z轴偏移量

1773213367307

1773213379910

手臂摆动的中轴向前偏移一定距离

1773214144609

因此C++改动为:

#pragma region 计算ArmMotion的Effector的RotationAmount值
	FRigUnit_GetArmMotionEffectorRotationAmount_Execute()
	{
		const float PerFootCyclePercent = PerFootCyclePercentArray[ArmIndex];
		//摆动的幅度
		const float ArmSwingAmplitude = MathFloatRemap(
										RigSpaceVelocity.Length(),
										0,
										500,
										0,
										45,
										true
										);
		//摆动的中轴向前偏移量
		const float ArmSwingAxisOffset = MathFloatRemap(
										RigSpaceVelocity.Length(),
										0,
										500,
										0,
										20,
										true
										);
		//ArmSwingAmplitude * sin( 2Π * (PerFootCyclePercent+0.25)%1 ) + ArmSwingAxisOffset
		const float ArmSwingCurve = sin(2 * UE_PI * FMath::Fmod(PerFootCyclePercent + 0.25f, 1.0f))
									* ArmSwingAmplitude
									+ ArmSwingAxisOffset;

		RotateAmount = AnimationCore::QuatFromEuler(FVector(ArmSwingCurve, 0, 0));
	}

效果:

1773214682336

修复:后退时同手同脚

1773216593401

因此C++改动为:

#pragma region 计算ArmMotion的Effector的RotationAmount值
	//计算ArmMotion的Effector的RotationAmount值
	USTRUCT(meta = (DisplayName = "GetArmMotionEffectorRotationAmount"), Category = "ArmMotion")
	struct PROCEDURALANIM_API FRigUnit_GetArmMotionEffectorRotationAmount : public FRigUnit
	{
		GENERATED_BODY()

		RIGVM_METHOD()
		virtual void Execute() override;

		UPROPERTY(meta = (Input))
		TArray<float> PerFootCyclePercentArray;

		UPROPERTY(meta = (Input))
		int ArmIndex;

		UPROPERTY(meta = (Input))
		FVector RigSpaceVelocity;

		UPROPERTY(meta = (Input))
		FQuat MovementAngleOffset;	//新增

		UPROPERTY(meta = (Output))
		FQuat RotateAmount;
	};
#pragma region 计算ArmMotion的Effector的RotationAmount值
	FRigUnit_GetArmMotionEffectorRotationAmount_Execute()
	{
		const float PerFootCyclePercent = PerFootCyclePercentArray[ArmIndex];
		//摆动的正负号(向后摆动时需要乘-1)
		const float ArmSwingSign = FVector::DotProduct(
			RigSpaceVelocity.GetSafeNormal(),
			MovementAngleOffset.RotateVector(FVector(0,1,0) )
			);
		//摆动的幅度
		const float ArmSwingAmplitude = MathFloatRemap(
										RigSpaceVelocity.Length(),
										0,
										500,
										0,
										30,
										true
										);
		//摆动的中轴向前偏移量
		const float ArmSwingAxisOffset = MathFloatRemap(
										RigSpaceVelocity.Length(),
										0,
										500,
										0,
										20,
										true
										);
		//Sign * ArmSwingAmplitude * sin( 2Π * (PerFootCyclePercent+0.25)%1 ) + ArmSwingAxisOffset
		const float ArmSwingCurve = sin(2 * UE_PI * FMath::Fmod(PerFootCyclePercent + 0.25f, 1.0f))
									* ArmSwingSign
									* ArmSwingAmplitude
									+ ArmSwingAxisOffset;

		RotateAmount = AnimationCore::QuatFromEuler(FVector(ArmSwingCurve, 0, 0));
	}

1773216966087

效果:

1773216940096

让向后移动时的手臂摆动幅度更小

1773221070305

因此C++改动为:

#pragma region 计算ArmMotion的Effector的RotationAmount值
	FRigUnit_GetArmMotionEffectorRotationAmount_Execute()
	{
		const float PerFootCyclePercent = PerFootCyclePercentArray[ArmIndex];
		//摆动的正负号(向后摆动时需要乘-1)

		//向后摆动时的幅度小一些
		const float ArmSwingSignClamp = FMath::Clamp(ArmSwingSign,-0.5,1);

		//摆动的幅度

		//摆动的中轴向前偏移量

		//ArmSwingSignClamp * ArmSwingAmplitude * sin( 2Π * (PerFootCyclePercent+0.25)%1 ) + ArmSwingAxisOffset
		const float ArmSwingCurve = sin(2 * UE_PI * FMath::Fmod(PerFootCyclePercent + 0.25f, 1.0f))
									* ArmSwingSignClamp 
									* ArmSwingAmplitude
									+ ArmSwingAxisOffset;

		RotateAmount = AnimationCore::QuatFromEuler(FVector(ArmSwingCurve, 0, 0));
	}

修复:手臂摆动小幅度向两侧打圈

把周期偏移0.25改为0.15

		//Sign * ArmSwingAmplitude * sin( 2Π * (PerFootCyclePercent+0.15)%1 ) + ArmSwingAxisOffset
		const float ArmSwingCurve = sin(2 * UE_PI * FMath::Fmod(PerFootCyclePercent + 0.15f, 1.0f))
									* ArmSwingSignClamp
									* ArmSwingAmplitude
									+ ArmSwingAxisOffset;

基于速度的肩膀晃动

1773220242746

-0.25是为了让肩膀晃动与跑姿同步:

  • 左脚落地左肩往下
  • 右脚落地右肩往下

1773222423021

迁移到C++:

#pragma region 计算肩膀的晃动偏移
//计算肩膀的晃动偏移
USTRUCT(meta = (DisplayName = "GetClavicleOffset"), Category = "ArmMotion")
struct PROCEDURALANIM_API FRigUnit_GetClavicleOffset : public FRigUnit
{
	GENERATED_BODY()

	RIGVM_METHOD()
	virtual void Execute() override;

	UPROPERTY(meta = (Input))
	FVector RigSpaceVelocity;

	UPROPERTY(meta = (Input))
	float MasterCyclePercent;

	UPROPERTY(meta = (Output))
	FVector ClavicleOffset;
};
#pragma endregion
#pragma region 计算肩膀的晃动偏移
FRigUnit_GetClavicleOffset_Execute()
{
	const float ClavicleZOffset = sin(2 * PI * 2 * (MasterCyclePercent-0.25) )
									* MathFloatRemap(
										RigSpaceVelocity.Length(),
										0,
										500,
										0,
										2,
										true
										) ;
	ClavicleOffset = FVector(0, 0, ClavicleZOffset);
}
#pragma endregion

1773222674642

效果:

1773222804874

Tweaks fixes and improvements

一些细节调整

移动时身体倾斜

1773300319602

1773300304976

迁移到C++

#pragma region 身体倾斜
	//身体倾斜
	USTRUCT(meta = (DisplayName = "PelvisLean"), Category = "OffsetPelvis")
	struct PROCEDURALANIM_API FRigUnit_PelvisLean : public FRigUnit_DynamicHierarchyBaseMutable
	{
		GENERATED_BODY()

		RIGVM_METHOD()
		virtual void Execute() override;

		UPROPERTY(meta = (Input))
		FVector RigSpaceVelocity;

	};
#pragma endregion
#pragma region 身体倾斜
FRigUnit_PelvisLean_Execute()
{
	URigHierarchy* Hierarchy = ExecuteContext.Hierarchy;
	if(!Hierarchy)
	{
		return;
	}

	FRigElementKey PelvisRig = FRigElementKey(TEXT("pelvis"), ERigElementType::Bone);
	FTransform TransformToRotate = Hierarchy->GetGlobalTransform(PelvisRig);
	FVector PointToRotateAround = TransformToRotate.GetTranslation();

	float LeanRotateAmount = MathFloatRemap(
		RigSpaceVelocity.Length(),
		0,
		500,
		0,
		-10,
		true
		);
	float RigSpaceVelocityYProjection = RigSpaceVelocity.GetSafeNormal().Dot(FVector::UnitY());
	float LeanRotateAmountAroundX = LeanRotateAmount * RigSpaceVelocityYProjection;
	//基于速度的前后旋转量(绕x轴)
	FQuat RotateAmount = AnimationCore::QuatFromEuler(FVector(LeanRotateAmountAroundX, 0, 0));
	//Pelvis自旋转后的Transform
	FTransform ModifiedTransform = RotateAroundPoint(TransformToRotate, PointToRotateAround, RotateAmount);


	float LeanOffsetAmount = MathFloatRemap(
		RigSpaceVelocity.Length(),
		0,
		500,
		0,
		10,
		true
		);
	// 基于速度的前后位置偏移量(y轴)
	float LeanOffsetAmountOnY =	LeanOffsetAmount * RigSpaceVelocityYProjection;
	ModifiedTransform.AddToTranslation(FVector(0, LeanOffsetAmountOnY, 0));

	//最终倾斜后的Pelvis
	FTransform FinalPelvis;
	FinalPelvis.SetRotation(ModifiedTransform.GetRotation());
	FinalPelvis.SetTranslation(ModifiedTransform.GetTranslation());
	FinalPelvis.SetScale3D(ModifiedTransform.GetScale3D());

	Hierarchy->SetGlobalTransform(PelvisRig, FinalPelvis);
}
#pragma endregion

效果:

1773303948948

身体随着不同脚部迈出而左右侧倾

1773386748922

1773386765378

效果:

1773387043254

修复:奔跑时由于脚部落点位置落后导致后腿绷直

FinalLegIK中的Clamp Sphere的圆心应该放在大腿Thigh处,而且这个Thigh的Transform不应该为初始值

1773394735431

修复:移动速度突变时脚部落点更新滞后A

脚部锁定位置加一个Clamp Sphere限制

1773394377776

1773394256303

增大Lerp速度

1773402015705

脚在地面停留的时间:速度越快,停留在地面的时间越长

1773402829485

修复:身体前后倾斜速度以及手臂运动速度过快

新建一个Lerp速度更慢的RigSpaceVelocity

1773648231733

1773532215013

1773532902670

效果:

1773648411694

修复:移动方向改变时腿部交叉

这个也是als的通病

FootMovementAngleOffsetLimit变量名字修改为FootRotationFactor:

1773534211490

1773534296191

可以发现当这个值为1时,脚部交叉现象几乎没了

因此,我们需要让FootRotationFactor的值是动态的

1773546770830

1773651943485

迁移到C++:

#pragma region 计算每个脚的RotationFactor
//计算每个脚的RotationFactor
USTRUCT(meta = (DisplayName = "CalculatePerFootRotationFactor"), Category = "FootRotation")
struct PROCEDURALANIM_API FRigUnit_CalculatePerFootRotationFactor : public FRigUnit
{
	GENERATED_BODY()

	RIGVM_METHOD()
	virtual void Execute() override;

	UPROPERTY(meta = (Input))
	FQuat MovementAngleOffset;

	UPROPERTY(meta = (Input))
	int FootIndex;

	UPROPERTY(meta = (Output))
	float FootRotationFactor;
};
#pragma endregion
#pragma region 计算每个脚的RotationFactor
FRigUnit_CalculatePerFootRotationFactor_Execute()
{
	const float ZAngle = AnimationCore::EulerFromQuat(MovementAngleOffset).Z;
	if (FootIndex == 0)
	{
		//每当左脚向右转:说明这时候是左脚在前的右向移动,让此时的FootRotationFactor = 0,也就是前腿不旋转
		FootRotationFactor = (ZAngle > 0) ? 0.5 : 0.9;
	}
	else if (FootIndex == 1)
	{
		//每当右脚向左转:说明这时候是右脚在前的左向移动,让此时的FootRotationFactor = 0
		FootRotationFactor = (ZAngle > 0) ? 0.5 : 0.9;
	}
	else
	{
		FootRotationFactor = 0.9;
	}
}
#pragma endregion

效果:

1773547733525

1773652262046

让膝盖的朝向略微受MovementAngleOffset影响

1773549842616

脚部适应斜坡角度

1773654387279

效果:

1773553602209

身体向下偏移量固定值改为向下射线的距离

1773819272037

1773819300282

效果:

1773820100843

修复:InputPose更换蹲姿时膝盖没有分开

初始的FootPole朝向Vector数组:

1773630484110

1773630602623

把原先SetFinalLegIK中的RotateVector固定值(0,10000,0)换为该数组

1773630424222

效果:

1773631123250

修改:ArmMotion中的肘部Pole朝向Vector也改为初始姿态的默认值

1773630780513

1773631076327

让脚部预测落点移动速度更加平滑

1773632470216x

修改VectorLerp函数逻辑:

在即将到达目标值时减慢速度

1773632703264

C++的修改:

FVector VectorLerpIndependentOnFrameRate(FVector InVector, FVector TargetVector, float BlendSpeed, float DeltaTime)
{
	const float LerpFactor = FMath::Clamp<float>(BlendSpeed * DeltaTime, 0, 1);
	FVector DeltaVector = (TargetVector - InVector) * LerpFactor;
	return InVector + DeltaVector;
}

所有用到该函数的地方需要更改BlendSpeed的值

侧向移动时的ArmMotion

手臂前后摆动受移动角度偏移的影响少一些

1773649932932

摆动幅度受速度沿正前方分量的影响,斜向移动时这个分量小,映射之后手臂摆动幅度会变小

1773650041918

因此,C++的改动:

		//摆动的正负号(向后摆动时需要乘-1)
		const float ArmSwingSign = FVector::DotProduct(
			RigSpaceVelocity.GetSafeNormal(),
			MathQuaternionScale(MovementAngleOffset, 0.4).RotateVector(FVector(0,1,0) )
			);
		//向后摆动时的幅度小一些
		const float ArmSwingSignClamp = FMath::Clamp(ArmSwingSign,-0.5,1);
		//摆动的幅度
		const float ArmSwingAmplitude = MathFloatRemap(
										RigSpaceVelocity.Length(),
										0,
										500,
										0,
										50,
										true
										)
										* FMath::Clamp(FMath::Abs(RigSpaceVelocity.Dot(FVector(0,0.4,0))), 0.4, 1);

效果:

1773651714895

增加:上半身的左右摆动

修改前:上半身没有左右摆动

1773652809091

修改后:

1773652882659

Improved foot traces and foot avoidance

优化脚部轨迹追踪、避免脚部交叉

修复:从高处落下时,脚部位置延后导致轨迹异常

解决方法:

1.预测脚部落点的节点中,如果Sphere Trace没有击中,那么沿用以前的值

1773657318473

2.限制脚的高度

1773657789360

效果:

1773657973728

修复:当两只脚踩在不同高度的平面时,一只脚浮空

按照更低的脚做SphereTrace检测脚踩平台位置,作为z轴PelvisOffset

1773820902619

1773820967560

效果:

1773822838334

修复:斜面上走路,脚部会绕z轴向左偏移一定角度

1773822645541

预测脚部落点的节点中:

最终的Rotation的绕z轴分量应当取自移动角度偏移MovementAngleOffset

1773823209862

效果:

1773823112627

修复:脚部在高平台边缘会穿模

在预测落地点的周围做多点Trace检测

绘制矩阵Trace检测

1773893018975

1773905851742

效果:

1773903315466

沿原始落点朝向向前二次Trace

Count先设置为1,然后在Trace的起止点的基础上(沿原始落点朝向)向前移动到前脚掌的位置(图中绿色框是前脚掌位置),做为二次Trace的起止点

1773905870706

1773903414081

Count设回3

1773904093386

效果:

1773904069652

输出最佳落点

初始化LowestResult

1773906877549

如果两次Trace结果的高度差<LowestResult,取第一次Trace结果

1773907006130

1773907025845

检测效果:

1773907140046

比对两次Trace结果的Z轴高度,优先选取更高的

1773910250371

效果:

1773909873509

比对两次Trace结果与原始落点的位置偏移量,优先选取偏移更小的

OffsetFactor影响比重更小,也就是原始落点的影响权重更大

1773911031245

输出SelectedLandingPoint作为最终的落点位置

先把RectangleFootLandTraces节点的输入FootLandingSpot的Translation属性换为每只脚的预测脚部位置

1773911443005

输出SelectedLandingPoint作为Trace后的预测脚部位置

1773911982440

这里有个bug,是因为脚部自定义曲线的InsertPoint3(也就是曲线的中点)的xy值直接用了基于MovementAngle的旋转结果,没有区分移动和静止,导致停下时角色的脚还会来回Lerp

回到CalculateFootSpline节点,计算起止点之间的偏移,当偏移量小于一定值,认为已经停下,InsertPoint3保留最原始的Point2和4的平均值:

1773914027163

加入Trace的击中判断

1773916491947

1773915920431

1773915974104

1773916533935

效果:

1773914295100

修复:脚部最终落点重叠

修改前:

1773975320871

传入FootIndex区分左右脚,左脚Trace向左偏移,右脚Trace向右偏移

1773974591653

从图中可以看出,左右方向对应x方向上的正负,因此在x方向上,左脚偏移1,右脚偏移-1

1773974772391

1773975236918

效果:

1773974943449

1773989144653

修复腿部交叉

如果移动方向偏移MovementAngleOffset在一个不恰当的时机Lerp到目标值,会出现腿部交叉

这个不恰当的时机就是左右脚循环的起步阶段,在这个阶段不让移动方向偏移,也就是让MovementAngleOffset的Lerp速度 = 0即可解决

改动:

1773976806257

1773977724002

1773977752510

因此,c++的改动:

#pragma region 计算移动角度偏移
	FRigUnit_GetMovementAngleOffset_Execute()
	{
		const float OriginalZAngle = AnimationCore::EulerFromQuat(
			FromTwoVectors(FVector(0,1,0),RigSpaceVelocity)
			).Z;

		//TargetZAngle
		float TargetZAngle = OriginalZAngle;
		if (OriginalZAngle > 100)
		{
			TargetZAngle = OriginalZAngle - 180;
		}
		else if (OriginalZAngle < -100)
		{
			TargetZAngle = OriginalZAngle + 180;
		}

		//Lerp速度
		float LerpSpeed = 6;
		bool IsBigAngleOffset = abs(TargetZAngle - AnimationCore::EulerFromQuat(MovementAngleOffset).Z) > 90;
		bool IsInStartMoment = FMath::Modulo(MasterCyclePercent * 2, 1) < 0.4;
		if (IsBigAngleOffset && !IsInStartMoment)
		{
			LerpSpeed = 0;
		}

		FVector LerpedVector = VectorLerpIndependentOnFrameRate(
			AnimationCore::EulerFromQuat(MovementAngleOffset),
			FVector(0,0,TargetZAngle),
			LerpSpeed,
			ExecuteContext.GetDeltaTime<float>()
			);

		MovementAngleOffset = AnimationCore::QuatFromEuler(FVector(0,0,LerpedVector.Z));
	}

让一条腿躲避另一条腿

让左脚始终在左侧,右脚始终在右侧

1774003880838

1774003897433

1774004546684

1774004610494

FootAvoidance节点迁移到C++:

#pragma region 避免脚部交叉
	//避免脚部交叉
	USTRUCT(meta = (DisplayName = "FootAvoidance"), Category = "CalculateFootTargetTransform")
	struct PROCEDURALANIM_API FRigUnit_FootAvoidance : public FRigUnit
	{
		GENERATED_BODY()

		RIGVM_METHOD()
		virtual void Execute() override;

		UPROPERTY(meta = (Input))
		FVector IdeaLocation;
		UPROPERTY(meta = (Input))
		int FootIndex;
		UPROPERTY(meta = (Input))
		FQuat MovementAngleOffset;
		UPROPERTY(meta = (Input))
		TArray<FTransform> SavedFootPlatformArray;

		UPROPERTY(meta = (Output))
		FVector ModifiedLocation;
	};
#pragma endregion
#pragma region 避免脚部交叉
FRigUnit_FootAvoidance_Execute()
{
	//前后移动方向矢量
	FVector MoveAngleVector = MovementAngleOffset.RotateVector(FVector::UnitY());

	//指向身体两侧的矢量
	FVector BodySideVector = AnimationCore::QuatFromEuler(FVector(0,0,90)).RotateVector(MoveAngleVector);


	//对侧脚踩位置相距身体两侧偏移多少
	float OppositeFootBodySideOffset = SavedFootPlatformArray[(FootIndex==0)? 1: 0].GetTranslation().Dot(BodySideVector);
	//左脚需要偏移的量
	float LeftFootOffset = FMath::Min(IdeaLocation.Dot(BodySideVector), OppositeFootBodySideOffset-15);
	//右脚需要偏移的量
	float RightFootOffset = FMath::Max(IdeaLocation.Dot(BodySideVector), OppositeFootBodySideOffset+15);
	//身体两侧方向上需要避开多少
	FVector BodySideAvoidOffset = BodySideVector*( (FootIndex==0)? LeftFootOffset: RightFootOffset );

	//移动方向上需要避开多少
	FVector MoveAngleAvoidOffset = MoveAngleVector * ( IdeaLocation.Dot(MoveAngleVector) );

	//最终输出的位置
	ModifiedLocation.X = (BodySideAvoidOffset+MoveAngleAvoidOffset).X;
	ModifiedLocation.Y = (BodySideAvoidOffset+MoveAngleAvoidOffset).Y;
	ModifiedLocation.Z = IdeaLocation.Z;
}
#pragma endregion

效果:

1774004961904

最终效果:

总结

优点:用程序的方式极其节省动画资产,节省工程存储空间,节省传统状态机运行时大量加载动画带来的繁重内存,相对足够写实,与环境交互性强,修改灵活,不需要维护一个庞大的状态机,对于小制作游戏来说是一个很好的选择

缺点:对于需要特殊张力表现的角色来说,适配性不好,因为对于这类角色来说为了表现张力,通常身体四肢的变换不是符合现实运作的,会有短暂的拉伸和形变,而该系统的ik部分极大限制了这种发挥。

1774005915408

posted @ 2026-02-19 00:40  EanoJiang  阅读(61)  评论(0)    收藏  举报