乳摇动画
April 7, 2025About 4 min
乳摇动画
有两种做法:
- 弹簧控制器
- 物理模拟
通常来说使用物理模拟的情况会多一些
弹簧控制器

使用
骨骼中需要有胸部骨骼:

在动画蓝图中分别对胸部骨骼添加添加Spring Controller
控制节点即可

可配置选项如下:

核心参数:
- SpringBone:指定要应用弹簧效果的骨骼
- SpringStiffness:弹簧刚度,值越大恢复越快
- SpringDamping:弹簧阻尼,控制运动衰减速度
- ErrorResetThresh:误差重置阈值,防止过度拉伸
其中Filter Channels
是设置可位移和旋转的通道。
工作原理
- 初始化:节点获取初始骨骼位置和旋转
- 物理模拟:
- 计算弹簧力(基于胡克定律(
F = -kx
)) - 应用阻尼力(与速度成比例(
F = -bv
)) - 考虑重力、风力等外力
- 计算弹簧力(基于胡克定律(
- 数值积分:使用Verlet积分等方法更新位置
- 约束应用:确保运动在限定范围内
- 结果输出:将计算的位置/旋转应用到骨骼
源码分析
源码在Engine/Source/Runtime/AnimGraphRuntime/Private/BoneControllers/AnimNode_SpringBone.cpp文件中
主要逻辑在FAnimNode_SpringBone::EvaluateSkeletalControl_AnyThread
中,这个函数的主要作用是根据物理弹簧模型计算骨骼的新位置和旋转,并将结果输出到OutBoneTransforms数组中。下面解析其工作原理:
1. 初始检查和设置
DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(EvaluateSkeletalControl_AnyThread)
ANIM_MT_SCOPE_CYCLE_COUNTER_VERBOSE(SpringBone, !IsInGameThread());
check(OutBoneTransforms.Num() == 0);
const bool bNoOffset = !bTranslateX && !bTranslateY && !bTranslateZ;
if (bNoOffset)
{
return;
}
- 声明性能计数器和多线程安全范围
- 检查输出数组是否为空
- 如果没有启用任何平移轴,则直接返回
2. 获取骨骼初始位置
const FBoneContainer& BoneContainer = Output.Pose.GetPose().GetBoneContainer();
const FCompactPoseBoneIndex SpringBoneIndex = SpringBone.GetCompactPoseIndex(BoneContainer);
const FTransform& SpaceBase = Output.Pose.GetComponentSpaceTransform(SpringBoneIndex);
FTransform BoneTransformInWorldSpace = SpaceBase * Output.AnimInstanceProxy->GetComponentTransform();
FVector const TargetPos = BoneTransformInWorldSpace.GetLocation();
- 获取骨骼容器和骨骼索引
- 计算骨骼在世界空间中的初始位置(TargetPos)
3. 初始化物理状态
if (RemainingTime == 0.0f)
{
BoneLocation = TargetPos;
BoneVelocity = FVector::ZeroVector;
}
- 如果是第一次运行,初始化骨骼位置和速度
4. 固定时间步长物理模拟
if(!FMath::IsNearlyZero(FixedTimeStep, KINDA_SMALL_NUMBER))
{
while (RemainingTime > FixedTimeStep)
{
// 物理模拟循环
}
}
- 使用固定时间步长进行稳定物理模拟
4.1 基础移动和重置检查
FVector const BaseTranslation = (OwnerVelocity * FixedTimeStep);
BoneLocation += BaseTranslation;
if (((TargetPos - BoneLocation).SizeSquared() > (ErrorResetThresh*ErrorResetThresh)))
{
BoneLocation = TargetPos;
BoneVelocity = FVector::ZeroVector;
}
- 根据所有者速度更新骨骼位置
- 如果偏离目标位置超过阈值,则重置状态
4.2 弹簧力计算
FVector const Error = (TargetPos - BoneLocation);
FVector const DampingForce = SpringDamping * BoneVelocity;
FVector const SpringForce = SpringStiffness * Error;
FVector const Acceleration = SpringForce - DampingForce;
- 计算弹簧力(胡克定律)和阻尼力
- 得到净加速度
4.3 速度积分
// 防止阻尼过大导致不稳定
if (SpringDamping > CutOffDampingValue)
{
double const SafetyScale = CutOffDampingValue / SpringDamping;
BoneVelocity += SafetyScale * (Acceleration * FixedTimeStep);
}
else
{
BoneVelocity += (Acceleration * FixedTimeStep);
}
- 积分计算新速度,包含安全机制防止数值不稳定
4.4 位置积分和限制
FVector const DeltaMove = (BoneVelocity * FixedTimeStep);
BoneLocation += DeltaMove;
// 应用位移限制
if (bLimitDisplacement)
{
// 限制最大位移
}
- 积分计算新位置
- 应用位移限制确保不会过度拉伸
5. 旋转计算
if (bUseRotation)
{
// 计算基于位移的旋转
FQuat AdditionalRotation = FQuat::FindBetweenNormals(ParentToTarget, ParentToCurrent);
// 应用旋转轴过滤
OutBoneTM.SetRotation(FQuat::MakeFromEuler(EularRot) * OutBoneTM.GetRotation());
}
- 如果启用了旋转,计算骨骼因位移产生的旋转
- 应用旋转轴过滤
6. 输出结果
OutBoneTransforms.Add(FBoneTransform(SpringBoneIndex, OutBoneTM));
- 将最终变换添加到输出数组
物理模拟

使用
骨骼中需要有胸部骨骼:

步骤如下:
- 打开角色
Physics Asset
- 旋转胸部骨骼创建
Sphere Body
- 设置球形大小
- 设置物理类型为模拟
- 打开约束
- 设置约束->锁住角度
- 设置约束->线性限制
- 设置约束->线性力度
两边胸都做上面操做即可