Yan's Studio.

基于动力学的骨骼动画技术综述

Word count: 3.3kReading time: 13 min
2023/07/02

原创性声明

本文为作者原创,在个人Blog首次发布,如需转载请注明引用出处。(yanzhang.cg@gmail.comhttps://graphicyan.github.io/)

DynamicBone (Unity)

INTRO

Dynamic Bone | Animation Tools | Unity Asset Store

DynamicBone是一个简单的基于模拟弹簧振子的算法实现树状柔体的物理模拟插件。虽然基于模拟弹簧振子运动的算法实现,但是DynamicBone各节点之间的距离实际上不会发生变化。比起弹簧,父子节点之间的相对运动更接近串联的单摆。

工作流

基本操作

绑定Dynamic Bone,设置对应的Root、Colliders、及相关动力参数。
image.png

属性参数

  • Damping (阻尼):阻止简谐运动的惯性运动,相当于弹簧的摩擦力。为0时简谐运动过程不会主动停止,为1时简谐运动过程不会发生。
  • Elasticity (弹性):决定回振移动强度,在简谐运动过程中作为额外的作用力将节点拉到还原位置,相当于弹簧的弹力。为0时系统形变不会主动还原,为1时形变不会发生。
  • Stiffness (刚性):限制最大振动幅度与方向,保证碰撞处理前节点不会跑到指定范围外,相当于弹簧的硬度。为0时不发挥作用,0到1时限制范围从2倍原始距离到0线性衰减。
  • Inert (惯性):限制形变幅度,在每一帧的简谐运动迭代发生前,无条件随物体整体运动拉动节点,拉动距离为Inert * 整体运动距离 。
  • Update (更新频率):DynamicBone计算频率,当游戏实际帧率高于这个更新频率时,DynamicBone会在每一帧进行消极计算,会尽量保持节点形状,但不会进行简谐运动模拟;当DynamicBone更新 频率远远高于游戏帧率的时候,DynamicBone会在脚本执行时尝试追帧,但每次最多执行4次,也就是更新频率实际最高只是当前游戏帧数的4倍。
  • Radius (半径):指定每个节点与DynamicBoneCollider发生碰撞的半径,注意节点互相之间不存在碰撞关系,这个半径是0碰撞依然会生效。
  • Damping\Elasticity\Stiffness\Inert\Radius各属性的Distrib:指定属性随着节点深度递增发生的变化;
  • End Length\End Offset 末尾节点偏移量:指定特殊的末尾节点End Bone局部位置。
  • Gravity 重力:在DynamicBone节点上施加的重力,方向是在全局坐标系中的,注意DynamicBone的重力比较特殊,只在节点运动发生时起效,会在节点运动时把节点向重力方向拉动。
  • Force 常驻力:在DynamicBone节点上施加的额外力,方向是在全局坐标系中的,注意Force与Gravity不同,是无条件生效的,会一直把节点向指定方向拉动。
  • Colliders 碰撞体列表:会与DynamicBone各节点发生碰撞的碰撞体对象。
  • Exclusions 排除节点列表:在设置Root节点后,DynamicBone会根据节点的GameObject的父子关系沿着子GameObject方向自动生成节点树,Exclusions中所有节点及其子孙节点都不会 生成DynamicBone节点。
  • Freeze Axis 固定轴:非None的情况下,所有节点在局部坐标系的对应的轴上在值不会发生变化。
  • DistanceDisable 距离控制开关:开启或者关闭距离控制机制,开启后如果DynamicBone所在的物体超出了参考物体的参考距离范围,DynamicBone的所有行为都会停止。
  • Reference Object 参考物体:距离制机制的参考物体,如果为空则DynamicBone会选择场景内的主摄影机作为参考对象。
  • Distance To Object 参考距离:距离控制机制的参考距离。

算法实现

概览

计算惯性->计算受力(verlet:重力、摩擦力、阻力)-> 模拟弹性 -> 刚性还原(刚性偏移、碰撞、修正)


流程细节

计算物体惯性


惯性模拟


记录此时,以备后续循环使用。

阻力模拟


受力模拟




弹性模拟

结合当前父节点的,算出该节点的理想位置


刚性模拟

代表该段弹簧原长,父子节点间原本的距离
为节点可偏离理想位置的最大距离
将节点限制在上述距离内

碰撞处理

提供sphere、capsule两种碰撞检测,如果嵌入,则直接penalty distance。

保持节点距离

防止父子节点之间发生拉伸或压缩,始终维持理想距离

修正旋转

计算原始子节点相对于父节点的位置
当前子节点相对于父节点的位置
让父节点的旋转与原始情况同步:

优缺点

优点

  • 参数少、使用友好。
  • 实现简单、性能相对还可以,全程显式计算。

缺点

  • 缺乏弹簧的性质,不能模拟容易拉伸形变的物体。
  • 计算过程没有时间概念(默认单位时间),导致调参难度大。
  • 碰撞处理较为简单,仅支持球和胶囊体、且仅作用于指定的碰撞体上。

优化方向

  • 性能:C++化、JobSystem、LOD降频
  • 碰撞方面优化。
  • 引入弹簧、隐式求解、XPBD(性能挑战)。

AnimDynamics (Unreal)

INTRO

AnimDynamics

AnimDynamics是UE4.11 Preview 5测试版本开始,发布的AnimationBlueprint中的新节点。功能是通过简单物理模拟计算,更新骨骼位置。优点是避免了使用纯物理模拟时计算量过大,并且能实现近似物理效果。尤其用于悬垂物(辫子,锁链等自由下垂物体)上可以获得非常好的结果。
为了实现低开销,AnimDynamics节点采用了一些值得留意的近似解算方法:

  • 使用盒体而非定义的动态骨骼来计算各节的惯性。
  • 不计算碰撞。相反,使用约束来限制移动。

工作流

基本操作

AnimDynamics节点支持线性约束、角约束和平面约束,以便模拟基于物理的的运动。线性(Linear)角(Angular) 约束可以受弹簧的驱动,提供更有弹性的感觉,而 平面(Planar) 约束可用于创建对象不会穿过的平面。

在AnimDynamics节点里,能够绑定RigidBody关联到指定Bone上,通过计算RigidBody的动量,在风,重力影响下更新动量,以及考虑附加的线性和角度的Limit,刷新Bone和RigidBody的位置。
通过启用 链(Chain) 属性,并选择 边界骨骼(Bound Bone)链端(Chain End) ,Anim Dynamics将使用两者之间的骨骼生成链。除了 边界骨骼(Bound Bone) 之外,链中的每块骨骼都将在其上方生成一个约束盒体,以模拟与链中其他骨骼的运动和碰撞。这些约束盒体需要手动调整才能实现不错的效果。

碰撞模拟

  • 平面限制:将平面限制添加到角色的 根骨骼 ,可以创建对象无法跨越的地面边界,以防结构看起来穿过了游戏世界的地面。

  • 球体限制:借助球体限制,你可以设置球体来包围模拟结构上的点,充当简单的碰撞预防,实现更动态的互动。


属性参数

  • **Chain **:是否使用链式Body绑定。若true则Bound Bone是Chain起点,Chain End是Chain终点,链中每个Bone都会绑上一个RigidBody。若false则只能选择BoundBone,对单个Bone绑定控制。
  • Bound Bone :Body绑定的骨骼
  • Chain End :使用链子时的终点
  • Box Extents :Body的ShapeBox的大小,用于计算惯性张量InverseTensor。Box体积大小与惯性大小成正比。
  • Local Joint Offset :Body相对于Bone的偏移值。偏移后类似于钟摆效果。
  • Gravity Scale :重力作用比例,重力大小使用UPhysicsSetting中的Gravity
  • Linear Spring :是否线性反弹,body会尝试弹回初始位置
  • Angular Spring :是否角度反弹,body会尝试和指定角度目标一致
  • Linear Spring Constant :计算线性反弹时的系数,越大反弹力越强
  • Angular Spring Constant :计算角度反弹时的系数,越大反弹力越强

算法实现

基于box的惯性更新

1
2
3
4
FVector Force = FVector(0.0f, 0.0f,UPhysicsSettings::Get()->DefaultGravityZ) * InBody->Mass *InBody->GravityScale;
Force += WindVelocity *InBody->WindData.WindAdaption;
InBody->LinearMomentum += Force *DeltaTime;
InBody->NextPosition =InBody->Pose.Position + InBody->LinearMomentum * InBody->InverseMass *DeltaTime;

使用约束限制移动

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
void FAnimNode_AnimDynamics::UpdateLimits(FComponentSpacePoseContext& Output)
{
...
const FAnimPhysBodyDefinition& PhysicsBodyDef = PhysicsBodyDefinitions[ActiveIndex];
...
if (PhysicsBodyDef.ConstraintSetup.bLinearFullyLocked)
{
// Rather than calculate prismatic limits, just lock the transform (1 limit instead of 6)
FAnimPhys::ConstrainPositionNailed(NextTimeStep, LinearLimits, PrevBody, ShapeTransform.GetTranslation(), &RigidBody, Body1JointOffset);
}
else
{
// 线速度
if (PhysicsBodyDef.ConstraintSetup.LinearXLimitType != AnimPhysLinearConstraintType::Free)
{
FAnimPhys::ConstrainAlongDirection(NextTimeStep, LinearLimits, PrevBody, ShapeTransform.GetTranslation(), &RigidBody, Body1JointOffset, ShapeTransform.GetRotation().GetAxisX(), FVector2D(PhysicsBodyDef.ConstraintSetup.LinearAxesMin.X, PhysicsBodyDef.ConstraintSetup.LinearAxesMax.X));
}
...
}

...
// 平面限制
if(PlanarLimits.Num() > 0 && bUsePlanarLimit)
{
for(FAnimPhysPlanarLimit& PlanarLimit : PlanarLimits)
{
...
FAnimPhys::ConstrainPlanar(NextTimeStep, LinearLimits, &RigidBody, LimitPlaneTransform);
}
}
// 球体限制
if(SphericalLimits.Num() > 0 && bUseSphericalLimits)
{
for(FAnimPhysSphericalLimit& SphericalLimit : SphericalLimits)
{
...
switch(SphericalLimit.LimitType)
{
case ESphericalLimitType::Inner:
FAnimPhys::ConstrainSphericalInner(NextTimeStep, LinearLimits, &RigidBody, SphereTransform, SphericalLimit.LimitRadius);
break;
case ESphericalLimitType::Outer:
FAnimPhys::ConstrainSphericalOuter(NextTimeStep, LinearLimits, &RigidBody, SphereTransform, SphericalLimit.LimitRadius);
break;
default:
break;
}
}
}
}

优缺点

优点

  • 轻量级、计算快。
  • 设计合理:骨骼惯性+约束,美术友好。

缺点

  • 碰撞检测不ok。
  • 约束调节需要经验。
  • 还是假。


OpenSource

JointDynamics

GitHub - SPARK-inc/SPCRJointDynamics: 布風骨物理エンジン
physics.gif

工作流

  • 为所有需要期待影响的骨骼添加 SPCRJointDynamicsPoint 组件。

SpcrJointDynamcis_1.png

  • 角色根节点添加 SPCRJointDynamicsController组件;并设置 Parent Transform。

SpcrJointDynamcis_2.png

  • 点击 Automatically detect the root points,自动配置子骨骼列表,并将根骨骼添加到列表中的子节点中。

SpcrJointDynamcis_3.png

  • 在Controller中,设置相关约束参数(弹簧等)。

SpcrJointDynamcis_4.png

  • 添加碰撞体。

SPCRJointynamicsColliderAdd.pngSPCRJointColliderSetting.png

参数列表

SPCRJointDynamicsEachParameter.png

算法实现

弹簧系统

SpcrJointDynamcis.png

大致流程
  • 获取节点移动及旋转等。
  • 更新节点碰撞体(capsule)。
  • 类似DynamicBone的方法,更新节点位置(惯性、摩擦力、重力、其他外力等)。
  • 处理表面碰撞(checkCollision->capusle的精确碰撞)。
  • 迭代求解约束,显式求解。
  • 处理碰撞体间碰撞。
  • 更新位置、记录速度。

image.png

特点

  • 使用JobSystem实现加速。
  • 考虑了柔体的弹性,保证效率同时,得到了还不错的结果。
  • 工作流相对简洁,但对使用者来说有一定成本,要添加节点、调整各种弹簧参数。
  • 效果会比DynamicBone更加丰富,但处理精细碰撞会导致效率更低

SoftBone

GitHub - EZhex1991/EZSoftBone: A simple kinetic simulator for Unity, you can use it to simulate hair/tail/breast/skirt and other soft objects
EZSoftBone_2.gifEZSoftBone_3.gif

  • All colliders supported (include MeshCollider)
  • Net structure supported (Cloth simulation)
  • Use EZSoftBoneMaterial to adjust effects, and reuse it on other EZSoftBones
  • Inherit EZSoftBoneColliderBase to create custom colliders
  • Beautiful wind simulator

工作流

  • 设置骨骼的变换设置骨骼材质(可复用)
  • 设置外部碰撞体
  • 设置系统外力(重力、风力等)

EZSoftBone_Inspector.png

算法实现

  • 通过Resistance、Damping、Stiffness、Slackness,更新bone的位移。
  • 通过弹簧约束,更新两端bone的位置(显式)。
  • 碰撞检测并进行响应。
  • 记录速度,更新位置。
    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
    Vector3 oldWorldPosition, newWorldPosition, expectedPosition;
    oldWorldPosition = newWorldPosition = bone.worldPosition;

    // Resistance (force resistance)
    Vector3 force = globalForce;
    if (forceModule != null && forceModule.isActiveAndEnabled)
    {
    force += forceModule.GetForce(bone.normalizedLength) * forceScale;
    }
    if (customForce != null)
    {
    force += customForce(bone.normalizedLength);
    }
    force.x *= transform.localScale.x;
    force.y *= transform.localScale.y;
    force.z *= transform.localScale.z;
    bone.speed += force * (1 - bone.resistance) / iterations;

    // Damping (inertia attenuation)
    bone.speed *= 1 - bone.damping;
    if (bone.speed.sqrMagnitude > sleepThreshold)
    {
    newWorldPosition += bone.speed * deltaTime;
    }

    // Stiffness (shape keeper)
    Vector3 parentMovement = bone.parentBone.worldPosition - bone.parentBone.transform.position;
    expectedPosition = bone.parentBone.transform.TransformPoint(bone.localPosition) + parentMovement;
    newWorldPosition = Vector3.Lerp(newWorldPosition, expectedPosition, bone.stiffness / iterations);

    // Slackness (length keeper)
    // Length needs to be calculated with TransformVector to match runtime scaling
    Vector3 dirToParent = (newWorldPosition - bone.parentBone.worldPosition).normalized;
    float lengthToParent = bone.parentBone.transform.TransformVector(bone.localPosition).magnitude;
    expectedPosition = bone.parentBone.worldPosition + dirToParent * lengthToParent;
    int lengthConstraints = 1;
    // Sibling constraints
    if (siblingConstraints != UnificationMode.None)
    {
    if (bone.leftBone != null)
    {
    Vector3 dirToLeft = (newWorldPosition - bone.leftBone.worldPosition).normalized;
    float lengthToLeft = bone.transform.TransformVector(bone.leftPosition).magnitude;
    expectedPosition += bone.leftBone.worldPosition + dirToLeft * lengthToLeft;
    lengthConstraints++;
    }
    if (bone.rightBone != null)
    {
    Vector3 dirToRight = (newWorldPosition - bone.rightBone.worldPosition).normalized;
    float lengthToRight = bone.transform.TransformVector(bone.rightPosition).magnitude;
    expectedPosition += bone.rightBone.worldPosition + dirToRight * lengthToRight;
    lengthConstraints++;
    }
    }
    expectedPosition /= lengthConstraints;
    newWorldPosition = Vector3.Lerp(expectedPosition, newWorldPosition, bone.slackness / iterations);

    // Collision
    if (bone.radius > 0)
    {
    foreach (EZSoftBoneColliderBase collider in EZSoftBoneColliderBase.EnabledColliders)
    {
    if (bone.transform != collider.transform && collisionLayers.Contains(collider.gameObject.layer))
    collider.Collide(ref newWorldPosition, bone.radius);
    }
    foreach (Collider collider in extraColliders)
    {
    if (bone.transform != collider.transform && collider.enabled)
    EZSoftBoneUtility.PointOutsideCollider(ref newWorldPosition, collider, bone.radius);
    }
    }

    bone.speed = (bone.speed + (newWorldPosition - oldWorldPosition) / deltaTime) * 0.5f;
    bone.worldPosition = newWorldPosition;

特点

与JointDynamics精神上基本一致,但没有实现JobSystem,力学方程相对简单。

KawaiiPhysics

assets/post_images/Dynamic-Bones/image_046.GitHub - pafuhana1213/KawaiiPhysics: KawaiiPhysics : Simple fake Physics for UnrealEngine4 & 5

是AnimDynamics的加强版,工作流与AnimDynamics一致,但效果更好。
KawaiiPhysics0.gifKawaiiPhysics1.gifKawaiiPhysics4.gif

思考

  • 基于约束的刚性及显式求解的精髓?
    • 稳定!稳定!还是稳定!快速!快速!还是快速! 牺牲真实感换来的安心。
  • 效果更好的Magic Cloth对比,性能开销大很多(数倍)。

Magica Cloth_UWA的博客-CSDN博客

  • 为什么要把旋转反作用回父节点?
    • 将子节点相对父节点的向量旋转,作用于父节点中,为了保持局部蒙皮的平滑,否则在节点处会有突兀感。

image.png
image.png

References

CATALOG
  1. 1. 原创性声明
  2. 2. DynamicBone (Unity)
    1. 2.1. INTRO
    2. 2.2. 工作流
      1. 2.2.1. 基本操作
      2. 2.2.2. 属性参数
    3. 2.3. 算法实现
      1. 2.3.1. 概览
      2. 2.3.2. 流程细节
        1. 2.3.2.1. 计算物体惯性
        2. 2.3.2.2. 惯性模拟
        3. 2.3.2.3. 阻力模拟
        4. 2.3.2.4. 受力模拟
        5. 2.3.2.5. 弹性模拟
        6. 2.3.2.6. 刚性模拟
        7. 2.3.2.7. 碰撞处理
        8. 2.3.2.8. 保持节点距离
        9. 2.3.2.9. 修正旋转
    4. 2.4. 优缺点
      1. 2.4.1. 优点
      2. 2.4.2. 缺点
      3. 2.4.3. 优化方向
  3. 3. AnimDynamics (Unreal)
    1. 3.1. INTRO
    2. 3.2. 工作流
      1. 3.2.1. 基本操作
      2. 3.2.2. 碰撞模拟
      3. 3.2.3. 属性参数
    3. 3.3. 算法实现
      1. 3.3.1. 基于box的惯性更新
      2. 3.3.2. 使用约束限制移动
    4. 3.4. 优缺点
      1. 3.4.1. 优点
      2. 3.4.2. 缺点
  4. 4. OpenSource
    1. 4.1. JointDynamics
      1. 4.1.1. 工作流
        1. 4.1.1.1. 参数列表
      2. 4.1.2. 算法实现
        1. 4.1.2.1. 弹簧系统
        2. 4.1.2.2. 大致流程
      3. 4.1.3. 特点
    2. 4.2. SoftBone
      1. 4.2.1. 工作流
      2. 4.2.2. 算法实现
      3. 4.2.3. 特点
    3. 4.3. KawaiiPhysics
  5. 5. 思考
  6. 6. References