前言
最近业务中用到Ogre做基于3D关键点虚拟角色骨骼驱动,但是遇到两个问题:
- 身体、头、眼睛、衣服等
mesh
的骨骼是分开的,但是骨骼结构都是一样的,需要设置共享骨骼 - 驱动的时候可以直接修改骨骼旋转量,或者将旋转量存到动画帧里面去,后者会根据播放时间间隔自动插帧
国际惯例,参考博客:
Ogre3D 实现角色换装
【Ogre-windows】旋转矩阵及位置解析
Ogre 换装系统 shareSkeletonInstanceWith
代码实现
下面分别包括:共享骨骼、关节驱动、动画帧驱动、遇到的坑
其中关节驱动和动画帧驱动方法所创建的运动为左小腿伸直弯曲,再伸直弯曲,再回到伸直弯曲,如此反复。
共享骨骼
核心函数是shareSkeletonInstanceWith
,能够指定将谁的骨骼共享给谁
但是需要注意,共享与被共享的骨骼具有同样的拓扑结构,不然会报错。
如果想强制共享,那就需要使用_notifySkeleton
,官方描述如下:
Internal notification, used to tell the Mesh which Skeleton to use without loading it.
@remarks
This is only here for unusual situation where you want to manually set up a Skeleton. Best to let OGRE deal with this, don't call it yourself unless you really know what you're doing.
意思就是说,告诉一个mesh
用另一个骨骼,但是不要轻易去用它,因为很容易出现问题,待会实验就知道了。
额外代码就不贴了,源码看文末就行。
首先读取三个模型:两个Sinbad.mesh
和一个jaiqua.mesh
//主模型
ent = scnMgr->createEntity("Sinbad.mesh");
SceneNode* node = scnMgr->getRootSceneNode()->createChildSceneNode();
node->attachObject(ent);
// 副模型1
ent1 = scnMgr->createEntity("jaiqua.mesh");
SceneNode* node1 = node->createChildSceneNode();
node1->setPosition(10, 0, 0);
node1->attachObject(ent1);//副模型2
ent2 = scnMgr->createEntity("Sinbad.mesh");
SceneNode* node2 = node->createChildSceneNode();
node2->setPosition(-10, 0, 0);
node2->attachObject(ent2);
然后共享骨骼:
ent1->shareSkeletonInstanceWith(ent);
ent2->shareSkeletonInstanceWith(ent);
会发现报错:
case Exception::ERR_RT_ASSERTION_FAILED: throw RuntimeAssertionException(number, desc, src, file, line);
就是因为jaiqua.mesh
与Sinbad.mesh
的骨骼不一样,所以对于jaiqua.mesh
必须增加:
ent1->getMesh()->_notifySkeleton(const_cast<SkeletonPtr&>(ent->getMesh()->getSkeleton()) );
如此便能成功运行了,如下图所示,左到右分别是:副模型2、主模型、副模型1;由于副模型1和主模型具有不同的骨骼,所以无法正常驱动。
共享骨骼的作用就在于:有时候同一个模型,分成了几部分设计,比如头和身体是分开的,便于将表情驱动和肢体驱动分开,但是它俩在设计的时候都是完整的人体骨骼,所以需要共享骨骼做一个同步。
修改关节旋转的驱动
动画帧驱动方法
分为两种,一种是一边创建一边播放,另一种是创建完毕再播放
先创建再播放
首先要知道你想创建的动画时长、帧率、播放速度,我这里为了测试帧的插值效果,创建了6s
的动画帧序列,首先初始化:
anim = skel->createAnimation("myanim", 6);
anim->setInterpolationMode(Animation::IM_SPLINE);
tracksnew = anim->createNodeTrack(lknee->getHandle(), lknee);
createAnim(); //创建动画帧//animation play
as = ent->getAnimationState("myanim");
as->setEnabled(true);
as->setLoop(false);
接下来就是创建动画帧,具体的创建方法,在之前的博客已经介绍过,这里直接贴代码:
void MyTestApp::createAnim() {for (int i = 0; i < 6; i++) {TransformKeyFrame *newKF = tracksnew->createNodeKeyFrame(i);Quaternion quat;quat.FromAngleAxis(Degree(i%2? 0.0f: -90.0f), Vector3::UNIT_X);newKF->setRotation( quat);prev_rotate = quat;}ent->refreshAvailableAnimationState();
}
注意创建完毕,要刷新一下动画的状态,不然修改无法生效。
最后在frameRenderingQueued
里面设置一下播放间隔:
as->addTime(0.033333);
表示每次播放接下来的0.0333帧数据,如果没有,就会自动插值出来。
一边创建一边播放
同样先在setup
里面初始化动画,但是记得刷新
// create animation
anim = skel->createAnimation("myanim", 6);
anim->setInterpolationMode(Animation::IM_SPLINE);
tracksnew = anim->createNodeTrack(lknee->getHandle(), lknee);
ent->refreshAvailableAnimationState();//animation
as = ent->getAnimationState("myanim");
as->setEnabled(true);
as->setLoop(false);
接下来直接在渲染主线程里面去写入动画帧,一边渲染一边写
// frame rendering
int i = 0;
bool MyTestApp::frameRenderingQueued(const FrameEvent &evt){ i++;TransformKeyFrame *newKF = tracksnew->createNodeKeyFrame(i);Quaternion quat;quat.FromAngleAxis(Degree(i%2? 0.0f: -90.0f), Vector3::UNIT_X);newKF->setRotation(quat);ent->refreshAvailableAnimationState();std::cout << as->getTimePosition() << std::endl;as->addTime(0.033333);return true;
}
这里需要注意一个问题,渲染是从第0帧开始的,但是你直接修改第0帧,这个数值在渲染进行结束前是无法生效的,也就是说在渲染线程里面修改的帧必须在当前帧渲染完毕才能生效,所以你修改的帧必须在当前渲染帧的后面,所以上述代码,直接修改的第1帧,并不是跟先创建动画后播放一样修改的第0帧。
直接修改关节旋转
非常简单,跟创建动画序列无任何关系,只需要在setup
中,将相关关节的setManuallyControlled
设置为true
SkeletonInstance *skel = ent->getSkeleton();
lshoulder = skel->getBone("Humerus.L"); lshoulder->setManuallyControlled(true);
lknee = skel->getBone("Calf.L"); lknee->setManuallyControlled(true);
然后再在渲染线程中修改骨骼旋转
int i = 0;
bool MyTestApp::frameRenderingQueued(const FrameEvent &evt){ i++;Quaternion quat;quat.FromAngleAxis(Degree(i%2? 0.0f: -90.0f), Vector3::UNIT_X);lknee->setOrientation(quat);return true;
}
因为这个渲染速度太快了,所以必须用断点才能看清每一帧的驱动效果,视频后半段是取消断点,一直驱动的结果
很容易发现,这种方法虽然简单,但是共享骨骼会失效,所以一旦使用此种方法驱动两套一样的骨骼,必须手动同步,把两套骨骼的所有关节setManuallyControlled
设置为true
,记住要删掉共享骨骼的代码先
for (int j = 0; j < skel->getNumBones(); j++) {skel->getBone(j)->setManuallyControlled(true);skel2->getBone(j)->setManuallyControlled(true);}
然后每次修改,都要同步每个关节遍历一遍,将两个骨骼对应关节同步好
for (int j = 0; j < skel->getNumBones(); j++) { skel2->getBone(j)->setOrientation(skel->getBone(j)->getOrientation());}
这样就可以同步运动啦,同样没有帧间平滑
注意坑
一定不要在帧动画驱动方法中,将骨骼的setManuallyControlled
设置为true
了,不然每一帧都是基于上一帧的结果驱动,正常的骨骼动画应该是类似于BVH
动画,每一帧都应该是独立的,且基于初始姿态的变换,比如A-pos或者T-pos,假设动画帧驱动的方法开启了手动控制,那么动画结果就是:
后记
本篇博文记录了工作中遇到了多个骨骼共享同一套动作的方法,同时这种方法都支持实时驱动,比如通过3D关键点计算得到旋转量以后,立马渲染出来。
后续应该会更新unity和Unreal Engine里面的肢体驱动方法,主要是将引擎与python通过socket通信传递深度学习提取的3D关键点,然后使用FABRIK或者其它动力学方法驱动虚拟角色,有兴趣可以关注一下。
本博文同步更新到微信公众号中,有兴趣可关注一波,代码在微信公众号简介的github
找得到,有问题直接公众号私信。