WOW m2模型与WowModelViewer

WOW m2模型与WowModelViewer
好长时间没学shader了,拿这个 物件的边缘高亮(Entity edge highlight) 练习了下,用render monkey,参照逍遥剑客的blog,很快就看到效果了,但是在现在的项目中实现的话有点麻烦,主要是对目前项目的Model还不熟悉,shader是怎么用的也不清楚,看了好几天,依然是迷迷糊糊,真他妈的菜啊!

现在的项目兼容m2模型,因为这引擎朝哥写的时候用的就是wow的资源,呵呵,山寨版wow。所以搞清楚了m2模型也就搞清楚了目前项目的model.开始看WowModelViewer.

MPQ Archives  The StormLib library



魔兽世界m2模型文件分析及wowModelView代码阅读心得

 

Title1、由于是浏览器,所以读取数据的函数再ModelViewer::OnTreeSelect方法中。选择分为角色模型和非角色模型,如下:
if (isChar) {
    modelAtt = canvas->LoadCharModel(rootfn.fn_str());
    canvas->modelType = MT_CHAR;
} else {
    modelAtt = canvas->LoadModel(rootfn.fn_str());
    canvas->modelType = MT_NORMAL;
}
其中的canvas是ModelCanvas *canvas,是ModelViewer类的一个属性;
然后ModelCanvas::LoadCharModel再调用ModelCanvas::LoadModel方法读取模型数据;
ModelCanvas::LoadModel新建了一个Modal,把传入的文件地址const char *fn传给了Modal的构造函数。
Modal的构造函数通过新建一个MPQFile类: MPQFile f(tempname);来读取模型文件数据;
MPQFile构造函数中用file.Read(buffer, size)实际读取了模型文件;
然后再Model::isAnimated中读取了骨骼、顶点等模型数据(通过从上一条中的 buffer=后来的header 中拷贝);

2、这个浏览器是用opgl来开发图像部分的,浏览器有一些坐标转换函数,浏览器中的转换函数在:
model.cpp中的Vec3D fixCoordSystem(Vec3D v)函数。
调用这个函数的函数是:Model::initCommon,initAnimated函数调用的它。
而initAnimated函数就在Model::isAnimated之后不久调用,被Model的构造函数调用。
  if (animated)
          initAnimated(f);

3、在initCommon中,依次读取vertices,normals,bounds(包括boundTris),textures
这些都经过了上一条说的fixCoordSystem坐标转换。

4、纹理是和顶点绑定的,一个顶点就有一个纹理坐标。读取顶点数据同时就要读取纹理坐标。
然后再实际渲染之前,设置相应的纹理对象。
这个是dx的纹理处理方案,opgl也应该差不多。

5、在initCommon接下来的继续读取中,我发现了一件事。那就是在读取attachments、colors和transparency的时候,initCommon先把数据读入ModelAttachmentDef、ModelColorDef和ModelTransDef等结构中,然后再读入到ModelAttachment、ModelColor和ModelTransparency这些具体的、直接可以使用的数据结构中。我推测带Def后缀的数据结构大部分都是一种过渡用的数据结构。

6、在initCommon之后,initAnimaited继续读取了bone、初始化了bones、texcoords、animTextures、particle systems(粒子系统)、ribbon、Camera和初始化了light。
在读取这些数据的时候,都用到了第5条所说的读取方式。

7、很多数据结构再读入数据后,里面都包含一种数据结构:Animated类。由于这写数据结构都与动画有关,所以每当读取玩自己的数据后都要调用Animated::init方法进行对自己相关的动画部分进行初始化。

8、推测AnimManager类是真正管理动画(播放、选择等)。推测ModelAnimation类读取得动画数据就是按照关键帧来排列的。

9、在ModelViewer::OnTreeSelect中load模型的所有数据之后,通过animControl->UpdateModel(canvas->model)来设置(不是渲染!)选择的非玩家角色模型;而通过charControl->UpdateModel(modelAtt)来设置(不是渲染!)玩家角色模型。
在第1条中(关于模型读取的部分),读取模型后,数据存放在Attachment*modelAtt和canvas->model中,然后如果设置(不是渲染!)非玩家角色模型就用nimControl->UpdateModel(canvas->model);设置(不是渲染!)玩家角色模型就用charControl->UpdateModel(modelAtt)。

10、真正的渲染模型工作是在ModelCanvas::OnPaint中调用ModelCanvas::Render。
然后ModelCanvas::Render中调用Attachment::draw;Attachment::draw中通过判定ModelCanvas的drawModel(bool变量,事先在ModelViewer::OnToggleCommand中给出,这个部分就是消息函数);然后Attachment::draw调用model::draw;model::draw调用Model::drawModel
最后的实际渲染在Model::drawModel中。

11、实际渲染模型的Model::drawModel中关键的参数passes,是在Model::initCommon的结尾处赋给的值,通过一个pass的值。

12、在人物自定义上,以DBCFile类为基类,CharHairGeosetsDB、CharRacesDB、CharFacialHairDB、CharClassesDB、HelmGeosetDB类为子类,利用DBCFile类的浏览器Iterator类,来操作人物的自定义。在CharControl::RefreshModel中。
for (CharHairGeosetsDB::Iterator it = hairdb.begin(); it != hairdb.end(); ++it)
这个选择人物的各种发型、面部特征的原理是,把这些特殊的发型、特征等做成一个数组,然后选好后显示其中的一个,其它的不现实,这样就达到了自定义的效果。
for (size_t j=0; j<model->geosets.size(); j++) {
      if (model->geosets[j].id == id) {
  model->showGeosets[j] = (cd.hairStyle==section) && showHair;
      }
}
在每个子类中的// Fields 部分,都是指的dbc.mpq中每个子项内部的字段。
DBCFile::begin把data中的数据读入Iterator浏览器类中

13、CharControl::UpdateModel被ModelViewer::OnTreeSelect调用,负责自定义角色的工作,第12条所说的。

14、 在animated.h文件中的inline T interpolate(const float r, const T &v1, const T &v2)函数将v1和v2做了线性插值。整个Animated::getValue就是为了做特定的类的插值有关键帧的信息。比如做平移的变换矩阵,在2个关键帧之间用这个getValve做差值,返回去就是做成了当前的平移变换矩阵
if (trans.used) {
      Vec3D tr = trans.getValue(anim, time);
      m *= Matrix::newTranslation(tr);
}

15、动画中的顶点变换是在Model::animate中(Model::draw调用),先根据数据产生bone的3中变换矩阵(这里可以确定wow用的是关键帧骨骼动画技术。同时,这个工作每帧都要做),然后用这3个矩阵乘以顶点,还有法线。定点变换代码如下:
// transform vertices
ModelVertex *ov = origVertices;   
for (size_t i=0,k=0; i<header.nVertices; ++i,++ov) {
      Vec3D v(0,0,0), n(0,0,0);
      for (size_t b=0; b<4; b++) {
  if (ov->weights[b]>0) {
      Vec3D tv = bones[ov->bones[b]].mat * ov->pos;
      Vec3D tn = bones[ov->bones[b]].mrot * ov->normal;
      v += tv * ((float)ov->weights[b] / 255.0f);
      n += tn * ((float)ov->weights[b] / 255.0f);
  }
      }
      vertices[i] = v;
      if (supportVBO)
            vertices[header.nVertices + i] = n.normalize(); // shouldn't these be normal by default?
      else
  normals[i] = n;
}

16、gl中设置环境光强度的函数是:glLightModelfv(GL_LIGHT_MODEL_AMBIENT, la)。其中la是一个Vec4D变量,是事先设定好的值,作为环境光的强度。这个函数在ModelCanvas::Render中被调用。
glEnable(GL_COLOR_MATERIAL)容许程序将材质颜色加入到当前颜色中,然后调用glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)来实际把材质颜色放入当前颜色中。接着,程序调用glColor来实际设置和操作当前颜色。
glLightf(light, GL_CONSTANT_ATTENUATION, 0.0f);
glLightf(light, GL_LINEAR_ATTENUATION, 0.7f);
glLightf(light, GL_QUADRATIC_ATTENUATION, 0.03f);
这3个语句设置了点光源和聚光灯的3个衰减因子(常数、一次系数、二次系数)

17、已经证实AnimManager::Frame就是一个动画帧序列中的某一帧,可以说是当前帧把。
更新这个Frame就可以实现动画,这里骨骼起了至关重要的作用。

18、TextureManager::add函数实际读取了纹理图片(BLP格式),在Model::initCommon中被调用

19、opgl的纹理绘制似乎是这样的:首先读取纹理文件,然后glGenTextures生成纹理对象,在要渲染之前调用glBindTexture绑定纹理,用glTexParameteri设置一些参数

20、Iterator类重载了->,重载函数返回的是(return &record)Iterator的一个属性,就是一个Record对象的指针;同时又重载了*,返回return record,所以能够让*i返回为Record对象

21、在CharControl::Init中初始化了hairdb、chardb等这些要读取dbc.mpq文件包的数据结构,通过DBCFile::open方法读取。而CharControl类的那些hairdb、chardb等对象都是在其构造函数中直接写明了自己所要读取的文件数据的路径,由于这些对象是DBCFile类的字类,所以直接在自己构造函数后调用父类的构造函数来把自己的读取文件数据的路径告诉父类(CharSectionsDB(): DBCFile("DBFilesClient\\CharSections.dbc") {}),以便让父类在其open方法中直接使用这个路径为他们准确的读取数据。
Character\Scourge\Male\ScourgeMale.m2
Record类是最终程序直接使用的dbc.mpq中数据的数据结构。
CharRacesDB::Record CharRacesDB::getByName(wxString name)
{
  for(Iterator i=begin(); i!=end(); ++i)
  {
    if (name.IsSameAs(i->getString(Name),false) == true)
      return (*i);
  }
  throw NotFound();
}
类似这样的程序段都是把当前的DBCFile放到i中,然后返回。这样Record类就能发挥储存器的作用。

22、自定义角色的外貌是通过cd这个变量,事先再UpdateModel里设定的,然后再通过RefreshModel读取实际的blp纹理文件。先通过CharSectionsDB::Record rec = chardb.getByParams这样的函数来从dbc.mpq的脚本文件里读取相应的纹理脚本数据;然后通过rec.getString翻译这些脚本语句,接着通过furtex = texturemanager.add来实际读取纹理数据
我只要只改写实际读取纹理的函数就可以了,就是在texturemanager.add中的LoadBlp函数,在这里应该改写成d3d的api:CreateTextureFromFileEx

23、Model::animate中的顶点变换(15中提到的),每个顶点被4个骨骼所影响,所以在变换的时候分别把影响每个顶点的骨骼矩阵都乘以顶点向量,然后在加合,这样就把4个骨骼的影响合在一起了。
for (size_t b=0; b<4; b++)
{
  if (ov->weights[b]>0)
  {
    Vec3D tv = bones[ov->bones[b]].mat * ov->pos;
    Vec3D tn = bones[ov->bones[b]].mrot * ov->normal;
    v += tv * ((float)ov->weights[b] / 255.0f);
    n += tn * ((float)ov->weights[b] / 255.0f);
  }
}
以上代码中的v和n,就是用来整合影响每个顶点的4个骨骼的影响的。

24、ModelGeoset结构就是设置人物角色自定义的数据结构。CCharModel::InitCommon函数的最后创建了一系列的ModelRenderPass结构,我推测每一个此结构的对象都是一种角色自定义中的一种具体的实例,每一个都是占据了顶点数据中的一段(pass.indexstart)

25、原来渲染角色的方式是通过依次渲染模型的sub部分来实现的,24中所说的ModelRenderPass结构就是用来储存每个sub部分的数据结构的。然后在实际渲染的时候(Model::drawModel函数负责实际渲染工作),通过其一个方法init(Model*m)来选择是否渲染当前的sub部分,在init方法的最后:
return m->showGeosets[geoset] && ( (ocol.w > 0) && (color==-1 || (ecol.w > 0)) );
其中的m->showGeosets[geoset]就是Model类中的选择人物模型的sub部分的标志结构,这个结构在CharControl的UpdateModel方法和RefreshModel中被设置,这2个方法自定义了模型的实际外貌

26、类Attachment就是装备(手上的武器和副手,或者披风等)的数据结构(里面包含一个model类指针),渲染的时候每个Attachment对象实例就包含一个Model对象实例,就是说单独依次渲染每个Attachment里的Model对象,然后根据主Model(人物角色Model)的坐标来给这些附属Model对象来进行世界坐标变换,这样就能解释Attachment::draw中的setup的作用(包含了ModelAttachment::setup),再ModelAttachment::setup中包含了一个世界坐标变换,就很有可能是根据人物角色的坐标来变换这些附属模型(手上的武器和副手,或者披风等)的坐标

27、在CCharModel::DrawModel中的
m_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST,
            0,
                          0,
            // number of vertices
            p.vertexEnd - p.vertexStart,   
                          p.indexStart,
            // number of primitives
            p.indexCount/3);
函数,最后一个参数,这里需要将p.indexCount除以3,因为这个indexCount是索引的数量,而不是要渲染的图元(三角形)的数量(3个顶点一个三角形)

28、CharTexture::compose函数,就是在CharControl::RefreshModel后部调用的、用来混合纹理的函数,有这样一段代码:
TextureID temptex = texturemanager.add(comp.name);
Texture &tex = *((Texture*)texturemanager.items[temptex]);
第一行用来从事先读取号、创建好的纹理储存器中(TextureManager类)通过从std::vector<CharTextureComponent>容器(事先add纹理层的容器类)通过纹理名字(comp.name)来读取id(temptex),然后通过这个id调用真正存储这纹理数据的TextureManager类中的纹理数据。

29、CharControl::RefreshModel()中的代码:
// select hairstyle geoset(s)
  for (CharHairGeosetsDB::Iterator it = hairdb.begin(); it != hairdb.end(); ++it) {
    if (it->getUInt(CharHairGeosetsDB::Race)==cd.race && it->getUInt(CharHairGeosetsDB::Gender)==cd.gender) {
      unsigned int id = it->getUInt(CharHairGeosetsDB::Geoset);
      unsigned int section = it->getUInt(CharHairGeosetsDB::Section);
      if (id!=0) {
        for (size_t j=0; j<model->geosets.size(); j++) {
          if (model->geosets[j].id == id) {
            //std::cout << "Hair:\t" << id << "\t" << section << "\t" << ((cd.hairStyle==section) && cd.showHair) << "\n";
            model->showGeosets[j] = (cd.hairStyle==section) && showHair;
          }
        }
      } else if (cd.hairStyle==section)
        bald = true;
    }
  }
CharHairGeosetsDB::Geoset这个field值的意思是:当前record属于角色模型那个sub类(比如头发?胡须?等等);
CharHairGeosetsDB::Section这个field值的意思是:当前record所储存的某类sub中是哪种,比如头发sub类中的第几种头型;

30、CharControl::RefreshItem()是用来处理头部、肩部、双手处的模型的——需要用另外的模型文件——相对于手套、鞋子和衣服等直接从人物模型本身就能获取的;
而CharControl::AddEquipment()则处理的是手套、鞋子和衣服等直接从人物模型本身就能获取的模型装备的添加、删除修改等

31、当用户选择了装备时候,调用CharControl::OnUpdateItem(),然后事件设为UPDATE_ITEM,执行如下代码:
switch (type) {
case UPDATE_ITEM:
  cd.equipment[choosingSlot] = numbers[id];
  if (slotHasModel(choosingSlot))
    RefreshItem(choosingSlot);
choosingSlot就是角色的装备槽代号,比如肩膀、头部、双手等等;cd.equipment[choosingSlot]就是用装备槽代号代表的装备序列号(每个装备单独的id号,是唯一的)


  WoWModelViewer分析

终于完成魔兽世界的换装系统

2009-7-12 Sunday

从google code上svn最新的wowmodelviewer,用vs2008生成直接通过,不需要任何改动!我靠!0.48e,0.5.08,……唉!折腾啊!
就是装个VS2008费劲啊!幸亏以前大林同志给了个VS2008的安装文件(现在找不到了),装在家里笔记本上体验了下然后一直没用,这次派上用场了!公司的外网机懒得装了!

 

你可能感兴趣的:(WOW m2模型与WowModelViewer)