Ogre共享骨骼与两种骨骼驱动方法

前言

最近业务中用到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.meshSinbad.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找得到,有问题直接公众号私信。

在这里插入图片描述

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风翼冰舟

额~~~CSDN还能打赏了

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值