经过一段时间的研究实现了如何将骨架放到目前的跟踪系统上来,将骨架模型移植到系统上,替代原来的棍状模型,也在网上看到有些人在尝试做这个东西(好像都是d3d的骨骼动画,基于opengl的没有),我把自己的制作方法简单介绍一下,希望能对正在做这方面的人有所帮助。

 如何制作一个可控制的人体骨骼模型

描述:本文提供一种将骨架动作矢量映射到人体骨架模型的一种方法,通过输入各个骨骼的当前方向,反馈给骨架模型,这样就实现了动画的效果。
     实验开发工具是VC6.0在OpenGL平台上开发完成。

阅读对象:

假定读者已经熟悉OpenGL编程,就算不熟悉,只要了解基本的旋转,平移,堆栈操作就好。
 假定读者已经了解基本的c++编程,其中需要了解递归的算法,递归的方法请参考一下数据结构吧。
 

制作过程:

第一步,3D模型准备

这一步骤的目的是提供分解的骨骼模型,它需要导出多个组成身体结构的文件,模型可以不用自己制作,只要到网上找找吧,应该很多,最好是是人体模型,如果用动物的模型也可以,不过需要自己定义映射骨架了,比如图中的骷髅模型是我从人体动画软件poser 5.0找到的。然后使用3d max 将身体的各个部位导出为3ds文件,这个步骤很简单,也不需要有什么3d max的基础。这里有一个小的技巧就是可以选中多个部分作为一个3ds模型导出,比如我需要将左右肩胛骨与脊椎骨肋骨作为同一个部分导出,这样可以将它命名为身体躯干(body)。这样我们就准备了各个3ds文件了,分别是:

身体躯干 BODY.3DS
头部     HEAD.3DS
左臂     LSHOULDER.3DS
右臂     RSHOULDER.3DS
左小臂   LELBOW.3DS
右小臂   RELBOW.3DS
左大腿   LTHIGH.3DS
右大腿   RTHIGH.3DS
左小腿   LFEET.3DS
右小腿   RFEET.3DS

这样这些组成部分就可以灵活的拼接出一个人体来了。

第二步,定义相关的核心数据结构

为了得到运动的各个身体部分数据信息,我们需要存储一些运动信息,主要有:
骨骼ID
骨骼关节的当前位置;r_x,r_y,r_z
骨骼之间的关系,例如手臂是躯干的延伸,而左小臂是左臂的延伸;PID,CID

我们可以通过下图来了解骨骼之间的结构关系


存放3ds文件位置;file_name_3ds
3ds模型的初始化方向;这个是比较抽象一点的概念,它是指从父节点指向子节点的方向,例如左小臂的初始位置是平放向下,那么对应的矢量就是 (-0.2,-1,0)

以下是数据结构部分:
class bone
{
public:
 int y;
 int x;
 int r_z;  //现实世界z坐标
 int r_y;
 int r_x;
 int rotated_X; //旋转后的坐标
 int rotated_Y;
 int is_marked; //是否已经标记
 int PID;   //父节点
 int CID;   //子节点,目前针对轴关节和膝盖有效
 float start_arc_x,end_arc_x; //相对父节点的x 左右方向转动角度限制
 float start_arc_y,end_arc_y; //相对父节点的y 上下方向转动角度限制
 float start_arc_z,end_arc_z; //相对父节点的z 前后方向转动角度限制
 double LengthRatio;
 char name[80]; //名称
 char file_name_3ds[180]; //3ds文件名称
 int ID;     
 bone(int ID,char *name,int PID);
 virtual ~bone();
 float bone_init_x,bone_init_y,bone_init_z; //初始化骨骼的矢量方向,3d max 模型
};

第三步,初始化骨架结构
在定义了bone的结构以后,我们定义一个skeleton类来在第一次初始化时加载这些结构,

obone =  bone (2,"head",1); //定义一个bone
 strcpy(obone.file_name_3ds,"head.3DS"); //设置它的3ds文件名
 obone.bone_init_x = 0;  //初始化骨骼的矢量方向
 obone.bone_init_y = 1;
 obone.bone_init_z = 0;
 bonevec.push_back (obone); //放入vector结构,这里用到了STL编程技术中的vector

以下是实现的部分代码:
skelecton::skelecton()
{
 float fy = 0.56f ;
 float ftx = 0.19f;
 float ffx = 0.08f;
 bone obone = bone (1,"neck",0); 
 bonevec.push_back (obone);

obone =  bone (2,"head",1); 
 strcpy(obone.file_name_3ds,"head.3DS");
 obone.bone_init_x = 0;
 obone.bone_init_y = 1;
 obone.bone_init_z = 0;
 bonevec.push_back (obone);

obone = bone (3,"rShoulder",1); 
 bonevec.push_back (obone);

obone = bone (4,"lShoulder",1); 
 bonevec.push_back (obone);

obone = bone (5,"rElbow",3);
 strcpy(obone.file_name_3ds,"rShoulder.3DS");
 obone.bone_init_x = fy;
 obone.bone_init_y = -1;
 obone.bone_init_z = 0;
 obone.CID = 7;
 bonevec.push_back (obone);

obone = bone  (6,"lElbow",4); 
 strcpy(obone.file_name_3ds,"lShoulder.3DS");
 obone.bone_init_x = -fy;
 obone.bone_init_y = -1;
 obone.bone_init_z = 0;
 obone.CID = 8;
 bonevec.push_back (obone);

//.............太长只给出部分的代码..........................

}

第四步,学习3ds公共的类CLoad3DS,可以用来载入显示模型

这个类是公用一个类,详细的类CLoad3DS的接口信息可以到一个open source项目里参考。http://scourge.sourceforge.net
http://scourge.sourceforge.net/api/3ds_8h-source.html
实际上在使用这个类时候,我做了一些修改,加了得到最大顶点的方法。这个在第五步会说明。

我们定义一个OpenGL的类来做模型控制类,负责载入模型,

CLoad3DS* m_3ds;

int OpenGL::Load3DS(int ID, char *filename)
{
 if(m_3ds!=NULL) m_3ds->Init(filename,ID);
 return 0;
}

然后在显示时候调用

int OpenGL::show3ds(int ID)
{
 m_3ds->show3ds(ID,0,0,0,2); 
 return 0;
}

第五步,使用递归方法分层次载入模型

这里是重点的内容了,让我们思考一些问题,实现骨骼会随着输入的方向而改变方向,需要做那些事情呢?
首先针对一块骨骼来考虑:
第一,我们需要让骨骼绕着它的节点旋转到输入的方向上

第二,我们需要知道骨骼目前节点的位置,才能旋转。可是我们知道骨骼会跟着它的父骨骼转动的,例如左小臂会跟着左臂转动,当身体转动时左臂也会跟着身体转动的,这里看起来像是有一个父子连动的关系,所以当前节点的位置会与它的父骨骼有关,父骨骼转动的角度,子骨骼也必须转动,所以这里自然想到了递归模型了,至于如何存储这些转动过程呢,还好openGL提供了glPushMatrix();glPopMatrix();那么所有的子骨骼必须包含在父骨骼的glPushMatrix();glPopMatrix();好了,这个变成

//递归实现3d现实
int skelecton::Render_skeleton_3D(int ID)
{
 
 glPushMatrix(); //开始记录堆栈
 joint_point = pgl->get_joint_point(ID); //找到节点位置 
 glTranslatef(joint_point.x,joint_point.y,joint_point.z); //坐标移到节点位置
 pgl->rotate_bone (vt1,vt2,vto);                          //旋转骨骼到指定的方向
 glTranslatef(-joint_point.x,-joint_point.y,-joint_point.z);//坐标移回来
 pgl->show3ds(ID); //显示模型

//遍历子节点
 for (theIterator = bonevec.begin(); theIterator != bonevec.end();
         theIterator++)
     {
    pbone = theIterator; 
    if((pbone->PID == ID) )
    {
     Render_skeleton_3D(pbone->ID); //递归调用
    }
 }

glPopMatrix(); //退出记录堆栈

}

剩下需要解决的问题就是如何找到节点位置。
寻找节点位置,
我们看到上面代码 get_joint_point(ID)就是找到节点了,其实如果不追求高的准确度,我们可以假设每个模型的最高的点即为骨骼的节点,当然这个假设前提是人体模型是正面站立的,手臂自然垂下,这样可以近似认为每个模型的最高的点即为骨骼的节点,这样函数就很简单了,这个方法是修改了Cload3ds类的方法,如下:

Vector3f CLoad3DS::get_joint_point(int j0)
{
 CVector3 LastPoint;
 Vector3f vect;
 LastPoint.y = -1000 ;
 if(j0==2) LastPoint.y = 1000 ;//头部节点朝下

// 遍历模型中所有的对象
 for(int l = 0; l < g_3DModel[j0].numOfObjects; l++)
 {if(g_3DModel[j0].pObject.size() <= 0) break;// 如果对象的大小小于0,则退出
  t3DObject *pObject = &g_3DModel[j0].pObject[l];// 获得当前显示的对象
    
  for(int j = 0; j < pObject->numOfFaces; j++)  // 遍历所有的面
  {
   for(int tex = 0; tex < 3; tex++)     // 遍历三角形的所有点
  {
   int index = pObject->pFaces[j].vertIndex[tex]; // 获得面对每个点的索引

if(j0==2)
   {
    if(pObject->pVerts[index].y < LastPoint.y )
    LastPoint = pObject->pVerts[index];
   }
   else
   {
    if(pObject->pVerts[index].y > LastPoint.y )
    LastPoint = pObject->pVerts[index];
   }
  }
  }
 
 }
 vect.x = LastPoint.x ;
 vect.y = LastPoint.y ;
 vect.z = LastPoint.z ;
 return vect;

}

比较特殊的是头部节点是通过脖子连接的,所以它是取最低的点。

现在解决最后的问题了,如何旋转了,具体来讲就是骨骼从原来自然的状态旋转到目前的方向,例如手臂从自然垂下变成抬起,垂下和抬起两个状态的矢量是不同的方向的,如何旋转呢? 这里就要用到了空间几何里的点积和叉积的概念了,简单来讲就是利用点积来求矢量夹角余弦,利用叉积来求两个矢量的法向量,如果你忘记了这些概念,可以回去参考一下高等数学书,这个连接也提供了一些资料,可以帮助理解http://www.gameres.com/Articles/Program/Visual/Other/shiliang.htm
然后呢,我们知道了两个矢量的夹角与它们的法向量,下面的事情就变得简单了,我们让骨骼原来的矢量以法向量为旋转轴,旋转一定角度,这个角度就是两个矢量的夹角,这样问题就解决了,所以这里的代码如下:

int OpenGL::rotate_bone(Vector3f vVector1, Vector3f vVector2, Vector3f vVectorOrgin)
{
 Vector3f vt1 =  Vector3f(vVector1.x,vVector1.y,vVector1.z);
 Vector3f vt2 =  Vector3f(vVector2.x,vVector2.y,vVector2.z);
 Vector3f vt4 = vt2-vt1;

double arc12 = AngleBetweenVectors(vVectorOrgin,vt4);
 double rarc12 = 180*arc12/pi;
 float len= Distance(vt1,vt2);
 Vector3f vt3 = Cross(vVectorOrgin,vt4);
 glRotatef ((float)rarc12,vt3.x,vt3.y,vt3.z);

return 0;

}

好了所有问题解决了,我们可以松一口气了。这里我提供一个参考的代码,因为这个模型的运行需要输入矢量方向,我目前还不能把它从我的其他系统程序完整提取出来,只是提供所有的代码,供读者参考。

下载源代码

教程:如何制作一个可控制的人体骨骼模型相关推荐

  1. html怎么制作小黄人,【PS教程】制作一个小黄人

    原标题:[PS教程]制作一个小黄人 小黄人已经是个无需介绍的人气角色,呆萌的外表让他成为了践踏主角的超级巨星,在全球拥有无数粉丝,作为喜欢小黄人的设计师,是不是一定要懂得3D软件才能做出小黄人哩?NO ...

  2. 表白小游戏教程:制作一个小游戏送给喜欢的TA(不会编程也能学会哦)

    大家好哇~ 欢迎翻开波波和阿菌的长篇故事~ 不过捏,今天我们不讲故事,今天将由阿菌和大家分享一个小教程:教大家制作一个小游戏,可以用来送给喜欢的人.无须任何计算机专业基础,只要能操作电脑,并在初一的数 ...

  3. ae怎么做圆一圈圈扩散效果_轻备学院AE特效基础教程 - 如何制作一个带有动画效果村庄烟雾...

    熟练运用AE软件做几个偏技巧性并且可以举一反三的超简单小动画,教大家如何制作一个带有动画效果村庄烟雾,你学会了这些小动画,不仅可以熟练运用AE软件,还能对动画制作有深入的了解. 第一步:在AI中绘制插 ...

  4. 制作一个Arduino控制的USB电源

    Arduino开发板非常适合自动控制各种电路.在本篇文章中,我将展示一种简单的方法,即Arduino可用于制作电源,自动调节其输出以维持恒定电压或恒定电流.我设计的这个电路采用USB供电,专为小型低压 ...

  5. 用java制作一个软件控制小车_Android手机控制智能小车的手机端程序(完整版)...

    [实例简介] 本程序是我写的Android手机控制智能小车的手机端的全部的源程序,下载后直接就能用. [实例截图] [核心代码] 624ba65e-a75e-4ba0-8e72-6dbc0823fcb ...

  6. 创建可微物理引擎Nimble,开源SOTA人体骨骼模型,斯坦福腿疾博士生用AI「助跑」人生...

    来源:机器之心本文约2000字,建议阅读5分钟 身残志坚,斯坦福大学的这位人工智能 + 机器人博士生想用技术克服身体缺陷. 有一位研究者,他身患残疾,出生时便患有退行性神经疾病,该疾病一直攻击着他的外 ...

  7. (译)如何使用cocos2d来制作一个打地鼠的游戏:第一部分

    免责申明(必读!):本博客提供的所有教程的翻译原稿均来自于互联网,仅供学习交流之用,切勿进行商业传播.同时,转载时不要移除本申明.如产生任何纠纷,均与本博客所有人.发表该翻译稿之人无任何关系.谢谢合作 ...

  8. 14. 如何使用cocos2d来制作一个打地鼠的游戏:第一部分

    免责申明(必读!):本博客提供的所有教程的翻译原稿均来自于互联网,仅供学习交流之用,切勿进行商业传播.同时,转载时不要移除本申明.如产生任何纠纷,均与本博客所有人.发表该翻译稿之人无任何关系.谢谢合作 ...

  9. 不需代码 open pose多人动态人体骨骼识别,面部,手部,姿势识别

    最近在学open pose相关的东西,OpenPose是卡内基梅隆大学的开源项目,它能支持2D和3D的多人关键点识别,支持手部.面部.脚部识别,单人运动姿势估计. 用官方提供的案例实现识别图片和视频中 ...

  10. 手把手教你如何用Python制作一个电子相册?末附python教程

    这里简单介绍一下python制作电子相册的过程,主要用到tkinter和pillow这2个库,tkinter用于窗口显示照片,pillow用来处理照片,照片切换分为2种方式,一种是自动切换(每隔5秒) ...

最新文章

  1. AngularJS基本知识点
  2. mysql员工脚本_mySQL常用脚本汇总
  3. 哎,最近心情非常烦乱!
  4. 二叉树C++ | 实现删除节点_4
  5. mysql null 0 空_MySQL中 null与not null和null与空值''的区别
  6. 带透明png转换成c数组
  7. 机器学习与数据挖掘_Regularization
  8. 【每日算法Day 78】面试经典题:能说出全部四种方法,不录用你都不可能!
  9. 第一行代码笔记-第五章
  10. 基于react的简单TODOList
  11. 正则表达式批量重命名
  12. java坦克加快速度_Java坦克大战 (三) 之可完全控制坦克朝八个方向运动
  13. 抖音便捷小空调特效 html+css+js
  14. Web渗透测试工程师:入门知识
  15. 结对开发项目--石家庄地铁web版
  16. Spring Framework灰度发布
  17. React Native入门-实战解析(上)
  18. vs2017配置opencv4.2及QTcreator配置opencv4.2在界面显示图像
  19. 2020最新免费网盘
  20. vue 路由重复跳转报错问题

热门文章

  1. sql服务器注册表被禁用,彻底删除SQL Server注册表信息
  2. kali mysql 卸载,linux mysql卸载命令
  3. 指数型组织到底是什么
  4. ERROR: unexpected error - Failed to connect to proxy URL: “http://127.0.0.1:8080/“
  5. Scala集合之Seq
  6. drupal 中page.tpl.php设置变量,Drupal主题开发教程 – 使用主题预处理函数为模板添加变量...
  7. 期权、期货及其他衍生产品 第二章 课程笔记整理
  8. NOIP2015初赛成绩
  9. 利用极域电子教室控制别人电脑
  10. 服务器桌面假死怎么处理,只需一分钟轻松搞定Win7桌面假死问题