全流程Pose Driver Deformer
根据主骨骼当前旋转角和Targets旋转角,插值出Blendshape权重或次级骨骼Transform
使用RBF插值,把Rm(旋转维度) ==> Rn(Target数量), 优点:平滑曲线、可以控制每个插值点的影响范围,既能做到局部影响也能做到全局影响
分解旋转为轴向(Twist)和非轴向(Orientation)
调研技术方案
Section titled “调研技术方案”针对一个rig,在旋转某些骨骼时会导致蒙皮mesh出现不自然的状况(比如手肘转动带动肌肉、弯腰带动衣服)。此时对mesh的修正主要有两种思路:
- blendshape:创建多个target,每个target有一个权重,动画师调整这些target的权重曲线(key帧 帧间B-Spline插值)。缺点:需要存储很多Mesh顶点信息,无论是内存还是计算时间开销都更大
- 次级骨骼:为需要的主骨骼添加多个次级骨骼,制作动画时,key好主骨骼之后,再key次级骨骼的Transform。
这两种的共同点是:
- 主骨骼旋转需要调整Mesh,需要动画师去编辑Mesh或编辑次级骨骼间接编辑Mesh。
- 在key的动画的过程中实现编辑Mesh,对于新的动画无法复用。
引入PSD,根据主骨骼旋转位置,插值出blendshape权重或次级骨骼Transform。在maya中使用手柄拖动时也能看到调整Mesh的生效,制作动画时只需要key主骨骼关键帧即可。
方案: 在游戏流水线中引入基于骨骼的PSD流程,打通 Maya/MotionBuilder/Unreal/Max
Reference:
- J.P.Lewis. Pose Space Deformation: A Unified Approach to Shape Interpolation and Skeleton-Driven Deformation, http://scribblethink.org/Work/PSD/PSD.pdf
- EigenSkin: Real Time Large Deformation Character Skinning in Hardware(https://zhuanlan.zhihu.com/p/205192636)
- Fast and Deep Facial Deformations:
- Skin Deformation Methods for Interactive Character Animation. 2017 Survey for skinning deformation methods:
Chad. https://www.chadvernon.com/tags/maya/
UE PoseWrangle
Section titled “UE PoseWrangle”https://www.unrealengine.com/marketplace/en-US/product/pose-driver-connect?sessionInvalidated=true
- 如果当前是psd设置为twist,但是有pose是swing得到的,会直接报错
- 如果权重都为0, 就用natrual pose
RBF Solver
Section titled “RBF Solver”是一种非线性插值,样本(以下称Samples)为
其中, n为样本个数
-
解算出
-
对于新输入的一个Query , 计算出
列举出几种实际应用中的取值
- 手柄拖动中间的cube移动,移动到四周的每个cube上时,旋转角应当和该处cube相同,是一个position(d1 = 3), 是对应于Samples每一个样本点的权重(d2=n)
- 角色动画中,拖动一个手肘关节旋转,次级骨骼随之进行平移/旋转/缩放,是一个手肘关节的欧拉角(d1 = 3), 是对应于Samples每一个样本点(称为pose)的权重(d2=n)

可以看出
- 在应用中一般 d2 == n, 即最终求到的函数值为每一个样本点的权重, 比如,第i位为1,其余为0
- 是一个把 映射为 的函数, 因此可以把看成一个矩阵
求解矩阵A的方法: 构建出待求解方程
(或其他度量距离方式)
最后的一个矩阵中,每一行是一个样本点(pose)的y, 形如[0, 0, …, 1, 0, …, 0], 解这个方程可以对A和Y矩阵拆分成列分别来求
对于每一个形如Ax=b, A为nxn, x为nx1, b为nx1的线性方程组, det(A)!=0时有唯一解,Micchelli定理保证了高斯核时,det(A)!=0,其他情况未保证【这里不确定,没有看过证明】,因此再解算的时候通过
这里一定是对称且正定的,再使用Cholesky分解进行解算。
RBF SDK实现
Section titled “RBF SDK实现”- 传入的x(样本) 和 KernalWidth(Sigma)是radiance欧拉角,
- 计算距离的结果是radiance,计算距离有两种
- 欧拉角的欧氏距离
- 四元数的球面角
- 计算phi(Distance(x),kernalWidth), 传入的kernalWidth和distance都是radiance的
- 解算
- 查询Query 传入radiance欧拉角
总的来说,把RBF SDK作为一个黑箱,输入的都是Radiance,输出的是 0 ~ 1的权重序列
template <typename value_t>struct EuclideanDistMetric : public DistMetricBase<value_t>{ using vector_t = typename DistMetricBase<value_t>::vector_t; value_t eval(const vector_t& A, const vector_t& B) override{ return (A - B).norm(); }};
/// return arc distance in radiancetemplate <typename value_t>struct ArcLengthMetric : public DistMetricBase<value_t>{ using vector_t = typename DistMetricBase<value_t>::vector_t;
vector_t Euler2Quaternion(value_t roll, value_t pitch, value_t yaw){ vector_t q(4); value_t cy = cos(yaw * 0.5); value_t sy = sin(yaw * 0.5); value_t cp = cos(pitch * 0.5); value_t sp = sin(pitch * 0.5); value_t cr = cos(roll * 0.5); value_t sr = sin(roll * 0.5);
value_t w = cr * cp * cy + sr * sp * sy; value_t x = sr * cp * cy - cr * sp * sy; value_t y = cr * sp * cy + sr * cp * sy; value_t z = cr * cp * sy - sr * sp * cy; q << x, y, z, w; return q; }
value_t eval(const vector_t& A, const vector_t& B) override{ // Euler to Quaternion vector_t A_quat = Euler2Quaternion(A[0], A[1] , A[2] ); vector_t B_quat = Euler2Quaternion(B[0] , B[1] , B[2] ); value_t dotValue = A_quat.dot(B_quat); auto ret = acos(2.0 * dotValue * dotValue - 1.0) ; return ret; }};/// Linear kernel: (sigma - d) / sigma if 0 < d < sigma , equal 0 otherwisetemplate <typename value_t>struct RBFLinear : public RBFKernelBase<value_t> { explicit RBFLinear( value_t sigma) : mSigma(sigma){}
value_t eval(const value_t &dist) const override { auto clamp = [](value_t left, value_t right, value_t x){ if(x < left) return left; else if(x > right) return right; else return x; }; return (mSigma - clamp(0, mSigma, dist)) / mSigma; }
protected: value_t mSigma;};
/// Exponential kernel: e^(-2 * d / sigma)template <typename value_t>struct RBFExponential : public RBFKernelBase<value_t> { explicit RBFExponential(value_t sigma) : mSigma(sigma){}
value_t eval(const value_t &dist) const override { return std::exp( -2 * dist / mSigma); }
protected: value_t mSigma;};
/// Cubic: max(1 - d^3 / (sigma^3), 0)template <typename value_t>struct RBFCubic : public RBFKernelBase<value_t>{ explicit RBFCubic(value_t sigma) : mSigma(sigma){}
value_t eval(const value_t &dist) const override { auto value = dist / mSigma; return std::max(1 - value * value * value, 0.0); }
protected: value_t mSigma;};
/// Gaussian kernel: e^(-d/(sigma*sigma))template <typename value_t>struct RBFGaussian : public RBFKernelBase<value_t>{ RBFGaussian( const value_t& sigma) : mSigma( sigma ) {}
value_t eval( const value_t& dist ) const { return std::exp( -dist / (mSigma * mSigma) ); }
protected: value_t mSigma;};大概是这几个函数
AutoWidth
Section titled “AutoWidth”所有的pose计算两两之间的距离,在求平均值。
计算Phi矩阵
Section titled “计算Phi矩阵”template <typename value_t>voidRBFSolverBase<value_t>::computeBasisMatrix( matrix_t& basisA ) const{ // A matrix of kernel weights basisA.conservativeResize(mSamplesAdded, mSamplesAdded); for (int col=0;col<mSamplesAdded;++col) for (int row=0;row<mSamplesAdded;++row) { value_t dist = mDistMetric->eval(mDataCoords[col], mDataCoords[row]); basisA(row,col) = mKernels[col]->eval( dist ); }}matrix_t basisA(mSamplesAdded, mSamplesAdded);this->computeBasisMatrix( basisA );// Use Cholesky factorizationmWeights.conservativeResize(mValueDim, mSamplesAdded);// Ax=b ==> A^T A x = A^T bEigen::LLT<matrix_t> solver( basisA.transpose() * basisA );for (int row=0; row<mValueDim ; ++row) { // for each row, b is basisA * [0, 0, .1, 0.., 0]^T = basisA.row(row) mWeights.row(row) = solver.solve(basisA.row(row).transpose());}Maya Node (骨骼版)
Section titled “Maya Node (骨骼版)”
技术美术: 对于一个既需要swing也需要twist的骨骼,可以采用下面这样的骨骼系统,就可以对两根不同的骨骼绑定两个psd了
- Swing Joint - Twist - {Twist Driven Joints} - {Swing Driven Joints}之前的方式: 通过xDecomposer来指定Orient/Twist, 如果是Twist就把joint的rotation关于twist axis的旋转去掉,输出到xSolver; 改为: 直接在xSolver中通过距离计算方式来实现【是否考虑twist】
补充上文关于距离计算的方法,额外增加两个:
- TwistAngle: 只考虑Twist
- SwingAngle:只考虑Swing
计算方法中最核心的部分在于,输入一个四元数q和一个twist axis,将这个q分解为swing和twist两个旋转(keyword: Swing-Twist decomposition for quaternion rotations) 参考博客:
- https://allenchou.net/2018/05/game-math-swing-twist-interpolation-sterp/
- https://www.chadvernon.com/blog/swing-twist/

// UE中的实现template<typename T>T TQuat<T>::GetTwistAngle(const TVector<T>& TwistAxis) const{ T XYZ = (T)TVector<T>::DotProduct(TwistAxis, TVector<T>(X, Y, Z)); return FMath::UnwindRadians((T)2.0f * FMath::Atan2(XYZ, W));}
vector_t getSwingAngle(const vector_t& q, const vector_t& axis) const { vector_t XYZ(3); XYZ << q.x(), q.y(), q.z(); value_t w = q[3]; vector_t projection = XYZ.dot(axis) * axis; vector_t twistQuat(4); twistQuat << projection.x(), projection.y(), projection.z(), w; if(twistQuat.norm() == 0.f){ for(int i = 0; i < 3; i++){ twistQuat[i] = 0; } twistQuat[3] = 1; }else { twistQuat.normalized(); }
return QuatMultiplyQuat(q, InverseQuat(twistQuat));}xDecomposer Node (不方便 不好做后续修改)
Section titled “xDecomposer Node (不方便 不好做后续修改)”对于Orient psd直接输出joint的欧拉角,对于twist psd根据轴向进行分解

- Input Rotate: Driver joint当前的欧拉角
- Output JointOrient:
xSolver Node
Section titled “xSolver Node”封装了RBF SDK
暴露一些用户自定义参数


- InputCoord: 当前的Driver的欧拉角 in Degree
- InputPoseList: 一组【xPoser Node】的输出,每个xPoser表示一个Pose的Driver的Transform矩阵
export psd
Section titled “export psd”json格式
Section titled “json格式”// PSD json格式{ "xxx_Solver":{ "DriverObject" : "arm_l", "DrivenObjects" : ["", "", ""], "DrivenJointOrientList" : [ [0, 0, 0], [0, 0, 0], [-80, 2, 3] ], "PSDType": 1, "BasisFunction": 1, "GlobalWidth": 90.0, "ClampNegativeWeight": true, "NormalizeMethod": 1, "TwistAxis": 0, "IsAutomaticRadius": true, "DistanceMethod": 0,
"Poses" : { "pose_1" : { "outDriverLocalMatrix" : [ (Mat4x4) ], "outDrivenLocalMatrixList" : [ (Mat4x4), (Mat4x4), (Mat4x4) ], "RawPoseRotation": [ -9.774415372647063e-06, 0.00020008505692559357, -119.99999999999999 ], } }
}}Max端插件
Section titled “Max端插件”提供四种可选语言:C++,C#, python,maxscript
wiki 整理:https://iwiki.woa.com/p/1356062572
参考仓库:https://git.woa.com/DCC_Client/Products/GoSkinning/3dsMax_go_skinning_cplusplus
使用Release模式 其他模式可能编不过
include目录
moc生成不了要去generatedfiles下面添加一个release/hybrid目录
Matrix Decompose to TRS
Section titled “Matrix Decompose to TRS”在maya中(应该所有的都是这样),一个affine变化对应的TRS顺序应该为,先Scale, 再Rotation,最后Translate。因为Translate对S和R有影响,我们希望S和R的pivot是原点/过原点的轴,所以最后Translate;Scale沿着xyz轴缩放(初始的时候物体的xyz和世界坐标对齐),所以先Scale。
如何分解?
// Decompose a matrix into TRS// 如果存在shear 就需要polar decompose// 存在shear == 缩放的轴和物体局部坐标系的轴没有对齐 == 先进行了旋转再进行了缩放(世界坐标系的缩放)// 但是在maya中拖动手柄进行的缩放操作都是按照局部坐标系操作的(等价于先缩放再旋转) ==> 不会有shear
// 参考UE中的FTransform(TMatix) 中会对TMatrix进行ExtractScaling (伪代码)trans = mat.GetTrans();scale = vec3{mat[0][0:3].norm(), mat[1][0:3].norm(), mat[2][0:3].norm()};rot = mat[0 : 3][0 : 3] / scale;
// 在3ds Max API中有DecomposeMatrix()函数对TRS顺序的Matrix进行分解ue插件文件结构
Section titled “ue插件文件结构”// 做一些省略,不区分Private和Public,省略文件名字的XPoseDriver前缀
- XPoseDriverEditor // 蓝图相关 - BPLibrary.h - BPLibrary.cpp
// 自动生成的? EditorModule.h EditorModule.cpp
// 继承UAnimGraphNode_Base UAnimGraphNode_XPoseDriver.h UAnimGraphNode_XPoseDriver.cpp
- XPoseDriverRuntime- ThirdParty - include / xPsd.h - lib / XPsdLib.lib通过distance method来制作swing和twist psd
Section titled “通过distance method来制作swing和twist psd”distance method是一个psd的属性,一个psd要么是swing要么是twist,对比三种distance method方法。可以对同一个joint分别建swing和twist两个psd。
- Euclidean: 两个pose的欧拉角的欧氏距离,会混合swing轴向和twist轴向的影响
- Quaternion: 四元数在球面上的夹角,会混合swing轴向和twist轴向的影响
- Swing: 四元数在球面上的夹角在非twist轴向的分量,不会包含twist
- Twist: 四元数在球面上的夹角在twist轴向的分量,只会包含twist
Euclidean

Swing

Twist

width和normalize method
Section titled “width和normalize method”- width: 用来控制每个pose的影响范围,如果核函数是Linear,离某个pose超过width范围时,该pose影响立刻变为0;而若采用Guassian核则会在超过pose后平滑递减直至0
- normalize methd
- OnlyNormalizeAboveOne: 对最外侧pose继续向外旋转会让权重逐渐下降(此时权重和会小于0,比如权重列表为[0, 0, …,0, 0.7])
- AlwaysNormalize: 对最外侧pose继续向外旋转会让权重保持(权重会一直为[0, 0, …, 1])
- 额外后处理:若所有pose的影响都为0,自动跳转到natural pose

