Skip to main content

Kawaii Joint Solver

Kawaii的网址:link

本文中的Kawaii Joint Solver为上一章节Joint Solver 整体框架中提到的类(我们将其命名为JointSolverBase)的子类。

class KawaiiJointSolver : public JointSolverVase { ... }

Kawaii本质上是一种利用父关节带动子关节运动的一种dynamics,结果会叠加在原动画上。

成员变量

首先我们可以先将Kawaii求解过程中需要的骨骼节点数据打包成一个结构体,方便后面的讲解:

struct KawaiiBoneData {
int boneIndex = -1;
int parentIndex = -1;
VECTOR3 location;
VECTOR3 preLocation;
VECTOR3 poseLocation;
KawaiiJointAttr* jointAttr;
}

其中需要注意,其中的locationpreLocation指的是仿真得到的数据,poseLocation是动画的数据。要注意区分仿真动画。 然后jointAttr是一个指向Kawaii求解器所管理的所有joint的相关属性,这些属性一般是由用户设定的,我们使用jointAttr这个指针来访问它。他一般包括下面这些内容:

struct KawaiiJointAttr {
float damping;
float worldLocDamping;
float worldRotDamping;
float stiffness;
float airflowDamping;
float limitAngle;
}

KawaiiJointSolver的成员变量包括:

    array<KawaiiBoneData>   _kawaiiBones;
VECTOR3 _compMoveVector;
QUAT _compMoveRotation;
Transform _preCompTransform;
float _exponent;
float _teleportDisThreshold; // 根节点的位移上限
float _teleportRotThreshold; // 根节点的旋转上限
float _windSpeed;
VECTOR3 _windDirection;
VECTOR3 _gravity;

// 基类中的成员变量:
// vector<VECTOR3> _jointPositions;
// vector<QUAT> _jointRotations;
// int _subStep;
// float _deltaT;
// float _alpha;
// DataInterface* _dataInterface;

成员函数

为了更好的实现功能,Kawaii求解器中定义了一些功能函数,我们首先对它们进行一些介绍。之后再来看看基类中的虚函数是怎么被实现的。

初始化骨骼节点的数据 InitBoneData()

之前我们将所有的骨骼节点的数据都封装进了一个结构体KawaiiBoneData中,所以首先我们当然需要对其进行一次初始化。

for (int jointIndex = 0; jointIndex < JointNumber; jointIndex++) {
int parentIndex = _dataInterface->GetJointParent(jointIndex);
KawaiiBoneData newBone;
newBone.boneIndex = jointIndex;
Transform boneTransform = _dataInterface->GetJointTransform(jointIndex);
newBone.location = boneTransform.GetLocation();
newBone.poseLocation = newBone.location;
newBone.preLocation = newBone.location;
newBone.jointAttr = (KawaiiJointAttr*)_dataInterface->GetJointAttr(jointIndex);
if (parentIndex < 0) {
// 如果不存在父节点
newBone.parentIndex = -1;
} else {
newBone.parentIndex = parentIndex;
}
_kawaiiBones.add(newBone);
}

修正根节点过大的位移和旋转 UpdateMovement(Transform& transform)

为了让整个模型更加稳定,我们会人为约束两帧之间根节点的位移和旋转,这个约束的阈值就是成员变量中的_teleportDisThreshold_teleportRotThreshold。 函数的输入transform就是当前根节点的运动信息,用于和_preCompTransform进行比对来找到两帧之间的位移和旋转,_preCompTransform的初始化在函数Init()中。

怎么找到相对的位移和旋转?

知道节点在某个坐标系下,两帧的旋转和位移之后,怎么计算得到两帧之间相对的旋转和位移?后一帧的位移/旋转叠加上前一帧的位移/旋转的逆运动即可。

void UpdateMovement(Transform& transform) {
// InverseTransformPosition 叠加逆位移运动
_compMoveVector = transform.InverseTransformPosition(_preCompTransform.getLocation());
if (_compMoveVector.Squared() > _teleportDisThreshold * _teleportDisThreshold) {
_compMoveVector = VECTOR3(0.0, 0.0, 0.0);
}
// InverseTransformRotation 叠加逆旋转运动
_compMoveRotation = transform.InverseTransformRotation(_preCompTransform.getRotation());
if (_compMoveRotation.GetAngle() > _compMoveRotation * _compMoveRotation) {
_compMoveRotation = QUAT::Identity;
}
// 更新_preCompTransform
// TODO 为什么不用_compMoveVector和_compMoveRotation构成新的_preCompTransform?
_preCompTransform = transform;
}

计算惯性和风力 UpdatePose()

如标题所示,计算惯性和风力并更新,有点类似于XPBD在求解约束之前,要先进行一次x=x+vtx=x+vt

for (int jointIndex = 0; jointIndex < JointNumber; jointIndex++) {
// 首先先将对应骨骼的数据取出来,由于我们是要对数据进行更新的,所以需要用引用
auto& bone = _kawaiiBones[jointIndex];
// 获取动画数据和仿真数据
bone.poseLocation = _dataInterface->GetJointTransform(bone.boneIndex);
bone.location = _dataInterface->GetJointSimTransform(bone.boneIndex);

if (bone.parentIndex < 0) {
// 如果没有父节点,那就让其直接跟随用户k帧的动画
bone.preLocation = bone.location;
bone.location = bone.poseLocation;
continue;
}

// 更新风力和damping的作用
VECTOR3 velocity = (bone.location - bone.preLocation) / _deltaT;
bone.preLocation = bone.location;
velocity *= (1.0f - bone.jointAttr->damping);

VECTOR3 windVelocity = _windSpeed * _windDirection;
velocity += windVelocity;

bone.location += velocity * _deltaT;

// 跟随根节点进行运动
bone.location += _compMoveVector * (1.0 - bone.jointAttr->worldLocDamping);
bone.location += (_compMoveRotation.RotateVector(bone.preLocation)-bone.preLocation) * (1.0 - bone.jointAttr->worldRotDamping);

// 重力
bone.location += 0.5 * _gravity * _deltaT * _deltaT;
}

约束仿真结果和动画之间的角度 AdjustAngle(...)

有时候动画师并不希望使用纯仿真的结果,他们希望动画的一切效果还是以自己k出来的为主,仿真只是锦上添花,所以说我们需要将仿真计算出来的结果进行约束。

void AdjustAngle(float limitAngle, VECTOR3& location, VECTOR3& parentLocation, VECTOR3 poseLocation, VECTOR3 parentPoseLocation) {
VECTOR3 boneDir = (location - parentLocation).Normalized();
VECTOR3 poseDir = (poseLocation - parentPoseLocation).Normalized();
VECTOR3 axis = VECTOR3::CrossProduct(poseDir, boneDir);
float angle = Atan2(axis.Length(), VECTOR3::DotProduct(poseDir, boneDir));
float angleOverLimit = angle - limitAngle;

if (angleOverLimit > 0.0f) {
// 将多余的部分转回去
boneDir = boneDir.RotateAngleAxis(_angleOverLimit, axis);
location = boneDir * (location - parentLocation).Length() + parentLocation;
}
}

接下来就是基类原有框架下的重写部分了。

初始化 Init()

除开基类已有的功能外,Kawaii的初始化中的额外工作就是将所有骨骼节点数据调用InitBoneData()进行初始化。

JointSolverBase::Init();
_kawaiiBones.Empty(); // 先清空
if (_kawaiiBones.Num() == 0) {
InitBoneData();
_preCompTransform = _dataInterface->GetComponentTransform();
}

隐式求解 SolveImpl()

Kawaii的隐式求解的过程大致如下:

  • 首先获取当前的根节点的位移和旋转,如果过大就将其修正
  • 应用惯性、重力和风力
  • 将仿真结果叠加到动画效果上
void SolveImpl() {
Transform componentTransform = _dataInterface->GetComponentTransform();
UpdateMovement(componentTransform);
UpdatePose();

int chainNumber = _dataInterface->GetChainNumber();
for (int chainIndex = 0; chainIndex < chainNumber; chainIndex++) {
int chainLength = _dataInterface->GetChainLength(chainIndex);
for (int chainNodeIndex = 0; chainNodeIndex < chainLength; chainNodeIndex++) {
int jointIndex = _dataInterface->GetChainNodeIndex(chainIndex, chainNodeIndex);
// 首先先将对应骨骼的数据取出来,由于我们是要对数据进行更新的,所以需要用引用
auto& bone = _kawaiiBones[jointIndex];
if (bone.parentIndex < 0)
continue;

auto& parentBone = _kawaiiBones[bone.parentIndex];
VECTOR3 poseLocation = bone.poseLocation;
VECTOR3 parentPoseLocation = parentBone.poseLocation;

// 如果没有仿真,本节点应该在的位置
VECTOR3 idealLocation = parentBone.location + (poseLocation - parentPoseLocation);
// 根据关节的刚性进行位置修正
bone.location += (idealLocation - bone.location) * (1.0 - pow(1.0 - bone.jointAttr->stiffness, _exponent));

// 修正角度
AdjustAngle(bone.jointAttr->limitAngle,
bone.location, parentBone.location,
bone.poseLocation, parentBone.poseLocation);

// 恢复长度
float boneLength = (poseLocation - parentPoseLocation).Length();
bone.location = (bone.location - parentBone.location).Normalized() * boneLength + parentBone.location;
}
}
}

为约束和碰撞求解做准备 PrepareSubStep()

在基类的方法中,对碰撞体的transform进行了插值,这里我们还需要对我们定义的骨骼节点数据进行插值:

virtual void PrepareSubStep() {
JointSolverBase::PrepareSubStep();
for (int jointIndex = 0; jointIndex < JointNumber; jointIndex++) {
auto* bone = &_kawaiiBones[jointIndex];
VECTOR3 targetLocation = bone->location;
VECTOR3 preLocation = bone->preLocation;
VECTOR3 dir = (targetLocation - preLocation) / (float)_subStep;
_jointPositions[jointIndex] += dir;
}
}

求解约束 SolveConstrains()

在约束求解以后,还需要对角度进行一次约束。

void SolveConstrains() {
JointSolverBase::SolveConstrains();
int chainNumber = _dataInterface->GetChainNumber();
for (int chainIndex = 0; chainIndex < chainNumber; chainIndex++) {
int chainLength = _dataInterface->GetChainLength(chainIndex);
for (int chainNodeIndex = 0; chainNodeIndex < chainLength; chainNodeIndex++) {
int jointIndex = _dataInterface->GetChainNodeIndex(chainIndex, chainNodeIndex);
auto& bone = _kawaiiBones[jointIndex];
if (bone.parentIndex < 0)
continue;
int parentIndex = bone.parentIndex;
auto& parentBone = _kawaiiBones[parentIndex];
AdjustAngle(bone.jointAttr->limitAngle,
_jointPositions[jointIndex], _jointPositions[parentIndex],
bone.poseLocation, parentBone.poseLocation);
}
}
}

基于上述所有内容,将对应的函数套用到JointSolverBase的求解框架中去,就可以使用Kawaii Joint Solver进行求解了。