首先:了解具体情况请看这里:https://github.com/wantnon2/3DToolKit-for-cocos2dx
在看代码之前,最好还是先把项目git下来执行一下demoproject:)
ToolKit的代码都在c3dToolKit文件夹下,demo test1的代码在Classes中。
以下通过对ToolKit中重点类进行分析来说明实现思路:
注:在写这篇教程的过程中。对代码进行了一些改动。所以假设已经得过代码的最好更新一下。
当前ToolKit中几个最重要的类是:Cc3dScene,Cc3dNode,Cc3dSprite。基本上这三个类引出全部的类。
一,Cc3dNode:
Cc3dNode是3D节点类,相当于cocos2d-x的CCNode。
Cc3dNode继承自CCNode,添加了下面数据成员:
Cc3dMatrix4*m_mat
Cc3dCamera*m_camera
m_mat是为4x4的3d变换矩阵,记录本节点相对于父节点的变换。
在CCNode中是用m_fRotation、m_fScale、m_fSkew、m_obAnchorPoint、m_obPosition等量来记录相对于父节点变换的(须要的时候再合成为矩阵)。当然,你也可以通过对上述成员作扩充使其可以描写叙述3D变换,只是出于简单我没有利用它们而是另外加入了m_mat来记录相对于父节点的3D变换。
m_camera为本节点所使用的相机的引用。
Cc3dCamera继承自CCCamera,因为CCCamera本身就是3D摄像机,我们直接继承它的set/getEyeXYZ()、set/getCenterXYZ()、set/getUpXYZ()和locate()等方法,便可实现正确的视图变换。
但还是进行了一些扩展,把投影变换的功能也增加到Cc3dCamera中(在cocos2d-x中投影变换的功能放在了CCDirector中)。并且像cocos2d-x一样,我们支持透视投影模式(perspective mode)和正交投影模式(orthographic mode)。
在test1 demo中能够切换这两种模式来观察效果。
在cocos2d-x中每一个节点都有自己的相机,能够对单个节点实现摄像机动画。
只是对于3D而言,这样的需求不存在。 同一个场景仅仅须要一个世界像机,因此Cc3dNode的m_camera成员仅仅是世界相机的引用。
正常情况下我们用3D场景根节点的像机作为此3D场景的世界像机。
重载visit函数。
Cc3dNode对CCNode的visit函数进行了重载。Cc3dNode::visit的逻辑与CCNode::visit基本一样,仅仅是将this->transform()换成了this->transform3D()。
transform()中是实现由m_fRotation、m_fScale、m_fSkew、m_obAnchorPoint、m_obPosition等计算变换矩阵并将其乘到模型视图堆栈的栈顶。类似地transform3D()实现将m_mat乘到模型视图堆栈的栈顶。
实现若干3D变换方法。
类似于CCNode的setPosition()、setRotation()等方法,Cc3dNode中实现了如setPosition3D(),translate3D(),translateRelativeToParent3D(); setRotation3D()。rotate3D(),rotateRelativeToParent3D()等(rotate3D的第一个參数是旋转轴的单位方向向量)。
值得注意的是XXX3D()和XXXRelativeToParent3D()之间的区别。
举个样例:如果定义了一个太阳节点。一个地球节点。并将地球节点加入为太阳节点的子节点。如果已经通过setPosition3D()设定好了地球相对于太阳的偏移,每帧调用earth->rotation3D(...)你会发现地球開始自转,每帧调用earth->rotateRelativeToParent3D(...)你会发现地球開始环绕太阳公转。(demo test1中对此有演示)。
类似于CCNode的nodeToWorldTransform(),worldToNodeTransform(),convertToWorldSpace()。convertToNodeSpace()等方法,Cc3dNode中实现了nodeToWorldTransform3D(),worldToNodeTransform3D(),convertToWorldSpace3D(),convertToNodeSpace3D()等方法。
3D变换不如2D变换直观,不熟悉矩阵变换的同学能够多做实验以摸清其行为。
isSceneNode3D方法。
nodeToWorldTransform3D()函数须要计算当前节点至3D场景根节点路径上全部节点的变换矩阵之积,我们须要使用Cc3dNode::isSceneNode方法来推断是否已到达3D场景根节点。
Cc3dNode::isSceneNode3D()方法默认返回false,对于3D场景根节点,我们重载使它返回true。
二,Cc3dScene:
Cc3dScene是3D场景根节点类,相当于cocos2d-x中的CCScene。
Cc3dScene继承自Cc3dNode。
重载isSceneNode3D方法。
前面已经提到了,3D场景根节点重载isSceneNode3D()使其返回true。
重载visit方法。
根节点的visit函数要比普通节点多做一些设置初始状态的工作。首先保存投影堆栈和模型视图堆栈的状态,然后使用本节点的像机的投影矩阵和视图矩阵重设矩阵堆栈栈顶。注意我们在切换到模型视图堆栈后先loadIdentity(),这表示我们不关心祖先节点的变换。强制回到世界原点,此后我们的3D场景从世界原点開始生长。
完毕以上初始化工作后再调用父类同名函数运行与普通节点同样visit代码。
3D场景的组织。
推荐的做法是将Cc3dScene节点加到一个CCLayer上,然后再在上面加入Cc3dNode、Cc3dSprite等节点。能够參考demo test1的Cscene3DLayer::init() 中的内容(在scene3DLayer.cpp文件里),那里我们创建了一个Cc3dScene和3个Cc3dSprite,并将它们组装成场景。
三,Cc3dSprite:
Cc3dSprite是3D精灵类,相当于cocos2d-x中的CCSprite。
Cc3dSprite继承自Cc3dNode。
主要添加了下面数据成员:
CCTexture2D* m_texture;
Cc3dMaterial* m_material;
Cc3dMesh* m_mesh;
Cc3dIndexVBO3d* m_indexVBO;
Cc3dProgram* m_program;
Cc3dUniformPassor* m_uniformPassor;
Cc3dLightSource* m_lightSource;
以上各指针成员指向的对象都可更换并遵从引用计数。
以上这些指针成员都有对应的get/set函数,表示这些指针成员所指向的对象是可更换的(比如更换mesh、更换shader program)。这样的可装配的特点用起来很灵活。另外这些对象都是CCObject的子类,遵从cocos2d-x的引用计数规则,因此是可共享的(比如一个mesh或shader program能够同一时候用于多个sprite)。
在Cc3dSprite::init()中为这些指针成员创建了默认对象。
m_texture就是cocos2d-x中的CCTexture2D。
像CCSprite一样,我们令一个Cc3dSprite仅仅包括一个texture。对于多texture的3d模型,能够使用多个Cc3dSprite,或者事先将纹理图片合并。
m_mesh提供顶点数据。
Cc3dMesh是一个索引三角网,数据成员例如以下:
vector<Cc3dVector4> m_positionList;
vector<Cc3dVector2> m_texCoordList;
vector<Cc3dVector4> m_normalList;
vector<Cc3dVector4> m_colorList;
vector<Cc3dIDTriangle> m_IDtriList;
当中m_positionList,m_texCoordList,m_normalList,m_colorList为顶点数据,给出3D模型的顶点信息。m_IDtriList为索引数据,描写叙述3D模型的拓扑结构。
在c3dDefaultMeshes.h中实现了若干基本几何体mesh的创建函数。
m_program继承自CCGLProgram,但在其基础上做了扩展。
Cc3dProgram在CCGLProgram基础上加入了一个uniform映射表,能够通过Cc3dProgram::attachUniform实现将GLint型的uniform句柄与uniform变量名绑定在一起。然后便能够通过对应的passUnifo函数实现按变量名为uniform变量传值,使用起来非常方便。
和cocos2d-x的CCGLProgram一样。Cc3dProgram能够通过传入vertex shader和fragment shader的文件名称来创建。并且与CCGLProgram一样,能够通过CCShaderCache::sharedShaderCache()->addProgram(program,key)将Cc3dProgram对象加入到CCShaderCache中。
眼下我在c3dDefaultPrograms.h/.cpp中创建了两个默认的shader program,一个是texOnly,一个是classicLighting,前者没有光照效果仅仅有纹理颜色,cocos2d-x中用的最多的便是这一款;后者是经典的光照shader。能够在橙宝书上找到。
对于不了解shader的同学,橙宝书是不错的入门教材。
m_indexVBO3d实现向显卡提交顶点和索引数据及发送绘制命令。
Cc3dIndexVBO3d成员包括下面几个buffer句柄:
GLuint m_posBuffer;
GLuint m_texCoordBuffer;
GLuint m_normalBuffer;
GLuint m_colorBuffer;
GLuint m_indexBuffer;
通过对应的submit函数能够将顶点数据或索引数据提交到对应的buffer中。
而Cc3dIndexVBO3d::draw()函数用来发送绘制命令。
一旦调用此函数,buffer中的数据便传入shader program进行绘制。
顶点数据又称为attribute数据,由于顶点数据终于要传给shader中的attribute变量。cocos2d-x 2.2中定义了一些内置的attributte变量,它们是a_position,a_color,a_texCoord。我们发现没有a_normal。然而对于3D画图来说法线属性是很重要的。计算光照必须用到法线。所以我们必须自己把这个属性加进去。加入方法能够在项目中搜kCCAttributeNameNormal和kCCVertexAttrib_Normals来查看相关代码。
另外注意在cocos2d-x中position是2维向量,而在3D中我们要用4维向量,因此在设置属性数据格式时我们要用glVertexAttribPointer(kCCVertexAttrib_Position, 4,…)而非glVertexAttribPointer(kCCVertexAttrib_Position, 2,…)。
至于shader中属性变量a_position的类型cocos2d-x用的本来就是vec4。
m_texture,m_material,m_lightSource,以及从Cc3dNode继承过来的m_camera提供uniform值。
我们知道shader program须要传入两类数据。一类是attribute数据(顶点数据),一类是uniform数据。顶点数据从VBO buffer中来(终于是从mesh中来),而uniform数据则从texture,material,lightSource,camera等对象中提取。这也正是我们要将lightSource和camera这种外部对象引入到Cc3dSprite之中的原因--这样我们便能够从Cc3dSprite中提取到全部所需的uniform数据。
Cc3dMaterial是材质对象,继承自CCObject,包括例如以下数据成员:
Cc3dVector4 m_ambient;//环境颜色
Cc3dVector4 m_diffuse;//散射颜色
Cc3dVector4 m_specular;//镜面颜色
float m_shininess;//镜面指数
emission(自发光颜色)因为尚未用到,临时还没有加入进来。
Cc3dLightSource是光源对象。继承自Cc3dNode。添加了例如以下数据成员:
Cc3dVector4 m_ambient;//环境颜色
Cc3dVector4 m_diffuse;//散射颜色
Cc3dVector4 m_specular;//镜面颜色
将Cc3dLightSource定义为Cc3dNode的子类,想法是将光源也看作一个节点挂到场景树上。
眼下光源的实现还过于简单。将来假设要实现不同类型的光源,还须要很多其它属性。
m_uniformPassor实现uniform传值逻辑。
Cc3dUniformPassor的数据成员是一个函数指针void (*m_callback)(Cc3dNode*, Cc3dProgram*)。成员函数是setCallback和executeCallback两个方法。
这样设计的目的是能够让用户自己去实现uniform的传值逻辑。
由于对于ToolKit框架而言。用户所使用的shader program有哪些uniform变量不可预知,用户的sprite有哪些成员数据不可预知(由于用户使用的可能是Cc3dSprite的子类。有新增的数据成员),所以uniform传值逻辑不可能通过ToolKit框架来解决,仅仅能留给用户自己去实现。用户写一个满足void (*m_callback)(Cc3dNode*, Cc3dProgram*)形式的回调函数实现uniform传值逻辑,然后将此回调函数set给uniformPassor。则uniformPassor便会在合适的时候调用executeCallback完毕传值。
回调函数有Cc3dNode*node和Cc3dProgram*program两个參数。意即从node中提取uniform值传给program的对应uniform变量。node能够通过类型转化转成用户所使用的Cc3dNode子类,从而訪问其数据。
uniform传值回调函数的实例能够參考c3dDefaultPassUniformCallback.h/.cpp。
一般是一个(sprite,program)组合相应一个callback。当为sprite更换shader program时应相应地更换callback(或uniformPassor)。
重载draw函数。
Cc3dSprite::draw()中大致完毕下面动作:
使用shader program:program->use().
为cocos2d-x的build-in uniform传值:program->setUniformsForBuiltins()
(注:cocos2d-x 2.2中的build-in uniform有:
CC_PMatrix
CC_MVMatrix
CC_MVPMatrix
CC_Time
CC_SinTime
CC_CosTime
CC_Random01
CC_Texture0
CC_alpha_value)
运行用户自己定义的uniform传值回调: uniformPassor->executeCallback(...)
绑定纹理
indexVBO发送画图命令: indexVBO->draw(…)。
于是,一个3D sprite就显示出来了。
----补充:
矩阵堆栈。
矩阵堆栈是OpenGL 1.0中内置的设施,但在OpenGL 2.0中去掉了,OpenGL 2.0因使用可编程管线使API得以大幅度简化。曾经固定管线的一些API和基础设施便被删掉除了,这是很合理的,由于核心越小越easy维护。外围的东西全然能够由用户自己去实现。cocos2d-x中实现了矩阵堆栈。有三个堆栈:projection matrix stack, modelview matrix stack, texture matrix stack,当中最经常使用的是前两个。
有若干操纵函数。当中较重要的有:
kmGLMatrixMode:切换堆栈
kmGLPushMatrix:复制当前栈顶矩阵并压入栈
kmGLPopMatrix:移除当前栈顶矩阵
kmGLLoadIdentity:将当前栈顶矩阵设成单位阵
kmGLLoadMatrix:将当前栈顶矩阵设成指定矩阵
kmGLGetMatrix:获得当前栈顶矩阵
我们知道递归本质上就是栈的操作,所以在递归渲染场景树过程中使用矩阵堆栈来记录变换状态是很好用的。
ToolKit中的矩阵和向量类。
矩阵类仅仅提供4x4矩阵c3dMatrix4;向量类仅仅提供2维和4维向量c3dVector2和c3dVector4。
对于3D而言,仅仅使用4维向量和4x4矩阵就好(w=0的4维向量表示空间向量,w=1的4维向量空间表示点。4x4矩阵描写叙述空间变换)。相信图形程序猿都习惯于这样的方式。所以我不打算定义3维向量和3x3矩阵了。那仅仅会添加混乱。
ToolKit原则。
不改动cocos2d-x代码。
尽可能符合cocos2d-x的思维和风格。
可读性>>性能。