前言
游戏引擎中对布料的模拟,通常采用基于物理方法的质点-弹簧模型(Mass-Spring Model)。在Unity模拟布料效果可以选择Cloth组件,Obi Cloth插件,还有模型进一步简化的Dynamic Bone、Swing Bone、PhysicsBone等插件。
为了实现定制的效果,本文将基于简化版的模型进行物理飘动的模拟。
这里是一篇物理模拟的文章,包含了本文中使用的大部分理论。
基本原理
定义粒子(Particle)节点,粒子是物理模拟的基本单位,物理模拟的结果就是驱动这些节点的位置变化。物理模拟包含的要素大致分为:外力结算、约束结算、碰撞结算,其中约束结算又包含了很多种约束。
这些物理模拟之间存在大量的耦合,比如物体受重力的同时又受到来自另一个粒子的弹性约束,两者之间是互相影响的。但对于游戏模拟而言,保证足够的可信度即可,可以容忍些许的精度缺失。所以采用松弛法(Relaxation)迭代,即每个方法独立结算,通过多次迭代达到较高精度。
物理模型
质点弹簧模型是将质点用弹簧连接,发生位移时,每个质点都受到弹性约束,这种模型的有点在于简单、效率高。
三种弹簧分别对应结构力(拉力、压力)、剪切力、弯曲力的结算。
Unity中的Cloth组件就是将模型的顶点作为质点,进而实现的飘动效果。
一种简化的链条模型
本文的实现方法类似Dynamic Bone、Swing Bone、PhysicsBone等插件,是将骨骼节点作为粒子(Particle),通过骨骼节点驱动蒙皮网格来实现飘动效果。由于质点的分布并非是整齐的,本文将从单链式的骨骼结构开始实现,不同于质点弹簧模型,这种结构没有质点弹簧模型的剪切弹簧。
本文先从最简化的结构开始,一个骨骼链条,将每个骨骼节点作为粒子(Particle)。
第一个粒子是根节点,根节点位置固定(挂载在其他运动的物体上,如动画系统),其他 粒子受本地形状约束,即每次变化粒子都会保持一定的本地拓扑结构。
物理模型在Unity中
先按照最简单的骨骼结构处理,只有父子级的单链结构,这里就不使用蒙皮网格了,可以在Unity中建立如下结构进行试验
这是由一系列胶囊体组成的层级父子级结构,模拟链式骨骼驱动模型运动,这里不能用球体代替,因为我们需要观察转动信息,蒙皮网格是被位置与转动共同驱动的。
Verlet Integration
Verlet算法是经典力学中的一种最为普遍的积分方法,被广泛运用在分子运动模拟(Molecular Dynamics Simulation),行星运动以及织物变形模拟等领域。Verlet算法本质上是对牛顿第二定律的泰勒展开,精度为O(4), 比欧拉方法精度更高,稳定度更好,且计算复杂度不比显式欧拉方法高多少。
简单的带阻尼Verlet Integration:
Verlet Integration的优势在于不必计算与保留速度信息,可以很方便的加入各种约束,缺点是每次泰勒展开的微元Δt必须是固定的,即每次迭代的时间不长是固定的。
本地形状约束
约束文中的链式结构的最简单方法是保持本地的拓扑结构,即强制让粒子回到父节点的原始相对位置(LocalPosition)上。
本文采用松弛法,所以可以自由添加约束,后文也会介绍更多约束类型。
碰撞结算
碰撞处理的最简单方法是将发生碰撞的点,移动至最近的碰撞表面,可以将碰撞视为约束的一种。
伪代码
using System.Collections.Generic;
using UnityEngine;
using System;
public class SpringarmParticleSystem : MonoBehaviour
{
/// 根节点
public Transform root;
/// 更新频率,每秒的次数
public float updateRate;
/// 碰撞集合
public SphereCollider[] colliders;
/// 粒子集合
public List<Particle> particles;
/// 定义基本粒子
public class Particle
{
/// 质点
public Transform transform;
/// 参数,多个
public float coefficient;
/// Verlex积分保存的位置
public Vector3[] positions;
/// 相对信息,多个
public Vector3 relativeValue;
}
void Start()
{
SetupParticles();
}
/// 初始化粒子组
private void SetupParticles(){}
private void LateUpdate()
{
UpdatesParticles();
}
/// 根据主循环帧时间确定迭代次数,迭代完成后应用更改,每帧只应用1次
private void UpdatesParticles()
{
for (int i = 0; i < iterationTime; i++)
{
UpdateParticles();
}
Apply();
}
/// 松弛法迭代
/// </summary>
private void UpdateParticles()
{
for (int i = 0; i < particles.Count; i++)
{
///VerletIntegration
VerletIntegration(i);
}
for (int i = 1; i < particles.Count; i++)
{
///本地形状约束
ShapeKeeping(i);
///约束
Resistance(i);
///约束
Resistance(i);
///碰撞
CollisionSolve(i);
///约束
Resistance(i);
}
}
}
处理后:
引入新约束
上面的伪代码中松弛法迭代过程中,VerletIntegration之后开始了一系列的约束,完成迭代之后进行Apply(应用更改)操作。整体的执行顺序是:
1、完成所有节点的运动学积分
2、完成所有节点的约束(每个节点的多个约束顺序执行,之后再执行下一个节点的所有约束)
3、应用变更
可以看出越靠后的约束越容易对显示的结果造成直接影响,所以我们要注意约束的顺序,例如在碰撞约束之后,执行长度约束,避免大碰撞体积导致形体拉伸。
for (int i = 0; i < particles.Count; i++)
{
///VerletIntegration
VerletIntegration(i);
}
for (int i = 1; i < particles.Count; i++)
{
///本地形状约束
ShapeKeeping(i);
///约束
Resistance(i);
///约束
Resistance(i);
///碰撞
CollisionSolve(i);
///约束
Resistance(i);
}
不同的约束能够组合出很多不同的效果,可以根据具体使用环境选取。
本地形状约束:约束父节点与本节点的相对位置
弹性约束:约束父节点与本节点的相对长度
弯曲弹性约束:约束二级父节点与本节点的相对长度
同心圆过长约束:约束根节点与本节点的相对长度
反向动力学
上文的方法只改变了子节点的位置,如果需要反向动力学,则需要让子节点对父节点产生影响,比如弹性约束中需要同时考虑一个节点的父节点与子节点对它的影响。
加入自定义约束
当需要模拟布料形态的物体时,文中的链式结构就无法满足需求了,所以我们增加一个节点与关联节点,在关联节点间增加约束。如下图类似裙摆的效果:
上面的演示是在相邻列中加入弹性约束(绿色线条),所以遇到碰撞的列发生变化也会影响到附近没有碰撞的列。