眼下的项目需要让奶子动起来!于是就有了这个……
先上理论文章
参考了AssetStore中的DynamicBone插件,作者Will Hong还给我指明了方向,真是巨大的帮助。
Advanced Character Physics这个文是Hitman:code47的动态骨骼碰撞方案
但是看起来Gamasutra上面的图都挂了,公式都是图……可以试试
[有图版本]
另外一篇思路相似的文[PositionBasedDynamics 主要是布料模拟和软蒙皮。思路是一样的
基本的思路
1. 位移去除速度v,单独计算速度不仅更慢而且并不稳定。
2. 迭代时间固定,要不然肯定不稳定
3. 弹性的表现依靠修正位移
**好处就是所有运算只跟位置相关,一切的一切都是算位置,简单直接粗暴**
位移公式
X_new = 2X-X_old + a * t * t
推导:
v * t = X - X_old v * t约等于上一帧的位移,带入
X_new = X + v * t + a * t * t 就排除了速度因素
实现
计算的对象
这个思路下计算需要知道上一次的位置,要算位移肯定要有力和质量
public class BoneParticle { /// <summary> /// 当前位置 /// </summary> public Vector3 position; /// <summary> /// 上一次的位置 /// </summary> public Vector3 prevPosition; /// <summary> /// 受力 /// </summary> public Vector3 force; /// <summary> /// 质量倒数 /// </summary> public float InvMass }
*这里只是最基本的,后面还会扩充*
物理模拟运算
1. 计算每个骨骼粒子受力情况
2. 计算目标的位置
3. 给粒子施加约束:弹性,碰撞.
void UpdateBoneChain (float delta) { AccumulateForces (); Verlet (delta); ApplyConstraint (); }
Verlet计算位置
整个计算就是之前的公式
damp是粒子的缓动效果
kinematic的节点不受弹性骨骼影响,位置由外部控制,且质量视为正无穷。比如动态骨骼链的第一个节点就必须是kinematic的
void Verlet (float deltaTime) { BoneParticle particle = null; Vector3 posCache; for (int i = 0; i < bones.Count; ++i) { particle = bones [i]; if (particle.kinematic) { particle.prevPosition = particle.position; particle.position = particle.transform.position; } else { posCache = particle.position; particle.position += (particle.position - particle.prevPosition) * (1 - particle.damp) + particle.force * particle.InvMass * deltaTime * deltaTime; particle.prevPosition = posCache; } } }
粒子的约束
对于动态骨骼来说,模拟的是有一定弹性的链条。
在Verlet之后这个链条会有很大的变形,模拟弹性就要根据弹性参数来调整链条的形状。弹性的表现由两种,一个是弯曲弹性/刚性,一个事线性弹性/刚性。
线性弹性
就是维持节点间距离的能力
p1,p2是当前点位置。d是原始长度
线性弹性就是要计算需要缩短的Δp1和Δp1。
|Δp1+Δp1| = ((p2-p1).length-d)
长度差系数diff = |Δp1+Δp1| /(p2-p1).length
//Stiffness_Strecth,线性刚性 //维持长度,将骨骼长度修复回原长度 Vector3 delta = bone.position - parentBone.position; float deltaLength = Mathf.Sqrt (Vector3.Dot (delta, delta)); float diff = (deltaLength - bone.boneInitLength) / (deltaLength * (parentBone.InvMass + bone.InvMass)); diff *= bone.stiffnessStretch;//最终需要缩减的长度系数 //按节点质量分配位置变化 parentBone.position += diff * delta * parentBone.InvMass; bone.position -= diff * delta * bone.InvMass;
注意:这里分配弹性的时候也使用了InvMass,如果是kinematic节点,InvMass是0,就不会被影响
弯曲刚性
就是位置节点间连线角度的能力。讲道理应该是这个样子
θ' = θ - stiffnessBend*(θ-α)
α是原始角度
不过计算角度显然很麻烦,我们用两步来趋近一下
1. 先用原始方向向量修改修改连线的方向
2. 再将长度恢复
感觉好像很麻烦,不过这样作的好处就是可以将两种弹性彻底分开,在调效果的时候就开心了。另外,可以跟线性弹性的运算一起优化。
//Stiffness_Bend,弯曲刚性 //弯曲刚性应该不影响长度,只影响方向,正常的算法是需要acos的,不过这里用两步来趋近 Matrix4x4 parentLocalToWorld = parentBone.transform.localToWorldMatrix; parentLocalToWorld.SetColumn (3, parentBone.position);//parent的位置可能是有变化的,修正坐标转换矩阵 Vector3 restPos = parentLocalToWorld.MultiplyPoint3x4 (bone.initLocalPosition); Vector3 bendStiffnessVector = restPos - bone.position; bone.position += bendStiffnessVector * bone.stiffnessBend; //此时两个particle之间的距离已经发生了变化,在计算线性刚性之前要修正一下,这个修正只影响子节点就可以 Vector3 bended = bone.position - parentBone.position; float bendedLength = Mathf.Sqrt (Vector3.Dot (bended, bended)); bone.position += bended * (originLength - bendedLength) / bendedLength;
碰撞
头发什么的总不能穿到身体里,这个思路下的另一个好处就是简单的碰撞只需要将粒子的位置设置到碰撞体边缘就行了。
当然,这样没有反弹,可是反弹也很容易。
先算得粒子插入碰撞的深度,然后根据碰撞的弹性参数将粒子位置再设置远一些。然后你就能看到反弹了。
在Unity中进行显示
先最简单的只修改位置
void ApplyTransform () { for (int i = 0; i < bones.Count; ++i) { BoneParticle bone = bones [i]; if (!bone.kinematic) { if (bone.transform != null) bone.transform.position = bone.position; } } }
这样跑起来的时候就已经很不错了,不过没有处理节点的旋转。对SkinnedMesh会看着比较僵硬,而直接挂上的cube就搞笑了。我们来修复一下旋转。
链式骨骼的旋转 = 链条形变导致的旋转 * 原始旋转
void ApplyTransform () { for (int i = 0; i < bones.Count; ++i) { BoneParticle bone = bones [i]; if (!bone.kinematic) { //fix rotation 有多个子节点的就忽略 if (bone.parent != null && bone.parent.children.Count <= 1) { BoneParticle parent = bone.parent; Vector3 dir0;//原始链条方向 if (bone.transform != null) { dir0 = bone.transform.localPosition; } else { dir0 = bone.initLocalPosition; } Vector3 dir = bone.position - parent.position;//当前链条方向 parent.transform.rotation = Quaternion.FromToRotation (parent.transform.TransformDirection (dir0), dir) * parent.transform.rotation; } if (bone.transform != null) bone.transform.position = bone.position; } } }