最近研究循环坐标下降(cyclic coordinate decent,CCD)算法,发现它在处理人物的某些关节上不起作用。CCD算法的原始算法针对的是多个骨骼、多个关节的IK解算处理,但是对于人体骨骼有着特殊的构造,使用CCD算法不能正确地反映这些构造,所以我们必须对这些构造进行特殊处理。
那么人体骨骼的构造特殊性在哪儿呢?这里我们暂且不探讨骨骼扭转的情况,仅仅探讨骨骼旋转的情况。我们发现人体的一些部位有三个自由度(3 Dimensions of Freedom,3DoF),而另一些部位只有一个自由度(1DoF),比如说我们的肩膀关节,它就有三个自由度,而肘部关节只有一个自由度;同理髋关节(如果可能的话)有三个自由度,而膝关节只有一个自由度。
了解这些很重要,因为我们要对CCD算法的结果进行进一步的限制。一般来说,普通的骨骼(比如说头发骨骼),当对末端关节进行移动以靠近固定关节时,二维的情况有两个解,三维的情况就有无穷多个解,解集会形成一个圆圈。我们只能看到我们的小腿相对于大腿顺着弯曲,却无法想象小腿相对大腿反着弯曲。这就是必须要限制的原因。
如果不限制的话,就会出现这样的情况:
正常情况是这样的:
那么如何对CCD算法进行限制呢?下面以膝盖为例。首先在预处理阶段判断骨骼的关节处是否为膝盖,保存为一个bool值,以后这样的骨骼进行单独处理。处理的方法是这样的:不以向量叉积求出的旋转轴进行旋转,而是以仅仅以X轴进行旋转。下面是改进后的代码:
/*---------------------------------------------------------------------------*/ void MMDRenderHandler::CalculateInverseKinematics( void )// 计算反向运动学 { // 遍历每个IK foreach ( const IK& _IK, m_IKs ) { Bone& destBone = m_Bones[_IK.destIndex]; // 一般是IK骨骼 Bone& targetBone = m_Bones[_IK.targetIndex]; // 一般是与IK一端连接的骨骼 for ( int i = 0; i < _IK.iteration; ++i ) { for ( int j = 0; j < _IK.bones.size( ); ++j ) { quint16 index = _IK.bones[j]; Bone& joint = m_Bones[index]; CCDIKSolve( joint, targetBone, destBone, _IK.limitAngle, i ); CalculateBonesFinalPos( index ); } if ( qFuzzyIsNull( ( targetBone.finalPos - destBone.finalPos ).SquareLength( ) ) ) { break;// 目的达到了,结束迭代 } } } } /*---------------------------------------------------------------------------*/ void MMDRenderHandler::CCDIKSolve( Bone& joint, // 想象成肘部 Bone& target, // 目标位置 Bone& end, // 末端效应器 float limitAngle,// 单位限制角度 int iterNum )// 迭代次数 { // 使用循环坐标下降算法(cyclic coordinate decent,CCD) // 计算在绝对旋转后的连接点和目标位置以及末端效应器的相对位置 Vector3F absJoint2End = end.finalPos - joint.finalPos; Vector3F absJoint2Target = target.finalPos - joint.finalPos; Quaternion invRotation = joint.absRotation.Conjugate( );// 求出四元数的共轭四元数 // 转为本地坐标系(平移因素在第一阶段已剔除) Vector3F localJoint2End = invRotation.RotatedVector( absJoint2End ); Vector3F localJoint2Target = invRotation.RotatedVector( absJoint2Target ); // 计算应该旋转的角度 float deltaAngle = acosf( Vector3F::DotProduct( localJoint2End.Normalized( ), localJoint2Target.Normalized( ) ) ); if ( std::isnan( deltaAngle ) || qFuzzyIsNull( deltaAngle ) )// 角度计算出错或角度太小(一般是向量太接近) { return;// 不处理,直接返回 } // 限制角度为[-limitAngle, limitAngle] deltaAngle = qBound( -limitAngle, deltaAngle, limitAngle ); // 求出旋转轴 Vector3F rotateAxis = Vector3F::CrossProduct( localJoint2Target, localJoint2End ); // 构造旋转四元数 Quaternion deltaRotation = Quaternion::FromRotation( rotateAxis, deltaAngle ); if ( joint.isXContraint )// 连接点的骨骼是膝盖,则限定仅绕X轴旋转 { float curYaw, curPitch, curRoll; float deltaYaw, deltaPitch, deltaRoll; if ( iterNum == 0 )// 第一次迭代仅仅绕着X轴旋转 { deltaRotation = Quaternion::FromRotation( Vector3F( 1.0f, 0.0f, 0.0f ), fabsf( deltaAngle ) ); } else { deltaRotation.ToEuler( deltaYaw, deltaPitch, deltaRoll ); joint.rotation.ToEuler( curYaw, curPitch, curRoll ); if ( std::isnan( deltaYaw ) || qFuzzyIsNull( deltaYaw ) )// 角度计算出错或角度太小(一般是向量太接近) { return;// 不处理,直接返回 } // 限制前滚角为[-0.002f - curYaw, M_PI - curYaw] deltaYaw = qBound( -0.002f - curYaw, deltaYaw, float( M_PI ) - curYaw ); // 进一步限制 deltaYaw = qBound( -limitAngle, deltaYaw, limitAngle ); deltaRotation = Quaternion::FromEuler( deltaYaw, 0.0f, 0.0f ); } } joint.rotation *= deltaRotation; joint.absRotation = m_Bones[joint.parent].absRotation * joint.rotation; }
演示程序下载地址:这里