在cocos2d-x中绘制3D图形--3D ToolKit for cocos2dx实现原理

应版主yuye-chukong之邀,写一下3D ToolKit for cocos2dx的实现原理。

3D ToolKit for cocos2dx是我花几天时间写的一个很小的cocos2dx的3D扩展。它是开源的,如果有兴趣,很容易看完全部代码。

了解详细情况请看这里:https://github.com/wantnon2/3DToolKit-for-cocos2dx
在看代码之前,不妨先把项目git下来运行一下demo工程:)

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 m_positionList;

vector m_texCoordList;

vector m_normalList;

vector m_colorList;

vector 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完成传值。

回调函数有Cc3dNodenode和Cc3dProgramprogram两个参数,意即从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和基础设施便被删掉除了,这是非常合理的,因为核心越小越容易维护,外围的东西完全可以由用户自己去实现。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的思维和风格。
可读性>>性能。

写得好辛苦哇,希望对大家有所帮助。

辛苦了。cocoachina介绍3d 制作的帖子很少。谢谢楼主的无私贡献。

— Begin quote from ____

引用楼主wantnon于2014-01-20 11:23发表的 在cocos2d-x中绘制3D图形–3D ToolKit for cocos2dx实现原理 :
应版主yuye-chukong之邀,写一下3D ToolKit for cocos2dx的实现原理。

3D ToolKit for cocos2dx是我花几天时间写的一个很小的cocos2dx的3D扩展。它是开源的,如果有兴趣,很容易看完全部代码。

http://www.cocoachina.com/bbs/job.php?action=topost&tid=181942&pid=tpc

— End quote

感谢楼主分享~ 顶!!

感谢楼主分享~ 顶!

:2:我下载运行一下,发现有一个错误 ,已经给楼主发github issue了。
https://github.com/wantnon2/3DToolKit-for-cocos2dx/issues/1

作者已修复,感谢大家的分享

— Begin quote from ____

引用第4楼子龙山人于2014-01-20 15:03发表的 :
:2:我下载运行一下,发现有一个错误 ,已经给楼主发github issue了。
https://github.com/wantnon2/3DToolKit-for-cocos2dx/issues/1 http://www.cocoachina.com/bbs/job.php?action=topost&tid=181942&pid=877780

— End quote

顶楼主!谢谢分享!!!

太棒了!!!希望能出个android能用的!

感谢楼主的无私奉献~

广州的开发者朋友可以加群一起讨论,群:370426060

这个太亮了啊.

想问下这个是基于cocos2d-x哪个版本下开发的啊?

牛叉 人士,
顶个赞

Hi 首先感谢楼主写的这个工具,很好用很强大。
另外,我目前有两个手机:小米1和小米2s,在工具中是用的glBindBuffer+glVertexAttribPointer来绘图的,但是这种方式在小米2s上可以,在小米1上却无法显示,小米1换成直接将数组参数发送给glVertexAttribPointer就可以显示了,由于我对opengl学习的不是很深,所以想请教下楼主,这个可能是哪方面的原因?

DAY DAY UP!:2:

:10: 够牛的说。。。。。。。。。。。。。。

找到原因了,去大神中文的git里,好像有教怎么配置的。

3.x的引擎,是不是把大神的这个加进去了

我是用cocos2dx 2.0里写的,我只配了ios 和win32工程.
你是在win32下运行?那可能是cocos 版本问题吧

我是楼主好友我自豪!

楼主厉害! 请问这个如果加载到ios的工程里,到时候 可以象原来一样转成anroid的嘛

@wantnon : 楼主 这个toolkit 能实现3d模型显示么? 我想在应用里使用3d的模型