cocos2d-x中绘制3D图形--3D ToolKit for cocos2dx实现原理
首先:了解具体情况请看这里: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的思维和风格。
可读性>>性能。
cocos2d-x中绘制3D图形--3D ToolKit for cocos2dx实现原理的更多相关文章
- SDL 开发实战(三):使用 SDL 绘制基本图形
在上文 SDL 开发实战(二):SDL 2.0 核心 API 解析 我们讲解了SDL最核心的API,并结合Hello World代码了解了SDL渲染画面的基本原理. 本文我们来讲一下,如何使用SDL的 ...
- Java入门:绘制简单图形
在上一节,我们学习了如何使用swing和awt工具创建一个空的窗口,本节学习如何绘制简单图形. 基本绘图介绍 Java中绘制基本图形,可以使用Java类库中的Graphics类,此类位于java.aw ...
- 【翻译】西川善司的「实验做出的游戏图形」「GUILTY GEAR Xrd -SIGN-」中实现的「纯卡通动画的实时3D图形」的秘密,后篇
http://www.4gamer.net/games/216/G021678/20140714079/ 连载第2回的本回, Arc System Works开发的格斗游戏「GUILTY G ...
- 使用OpenGL ES绘制3D图形
如果应用定义的顶点不在同一个平面上,并且使用三角形把合适的顶点连接起来,就可以绘制出3D图形了. 使用OpenGL ES绘制3D图形的方法与绘制2D图形的步骤大致相同,只是绘制3D图形需要定义更多的 ...
- Python 使用 matplotlib绘制3D图形
3D图形在数据分析.数据建模.图形和图像处理等领域中都有着广泛的应用,下面将给大家介绍一下如何在Python中使用 matplotlib进行3D图形的绘制,包括3D散点.3D表面.3D轮廓.3D直线( ...
- Python绘制3D图形
来自:https://www.jb51.net/article/139349.htm 3D图形在数据分析.数据建模.图形和图像处理等领域中都有着广泛的应用,下面将给大家介绍一下如何使用python进行 ...
- Python plot_surface(Axes3D)方法:绘制3D图形
3D 图形需要的数据与等高线图基本相同:X.Y 数据决定坐标点,Z 轴数据决定 X.Y 坐标点对应的高度.与等高线图使用等高线来代表高度不同,3D 图形将会以更直观的形式来表示高度. 为了绘制 3D ...
- Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第六章:在Direct3D中绘制
原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第六章:在Direct3D中绘制 代码工程地址: https://gi ...
- 去除WPF中3D图形的锯齿
原文:去除WPF中3D图形的锯齿 理论上讲PC在计算3D图形的时候是无法避免不出现锯齿的,因为3D图形都是又若干个三角形组成,如果3D图形想平滑就必须建立多个三角形,你可以想象一下正5边形和正100边 ...
随机推荐
- AIX 10201 ASM RAC安装+升级到10204
1:查看系统版本 [rac1:root:/hacmp/hacmp5.4/ha5.4/installp/ppc] oslevel -s 6100-06-06-1140 lslpp -al bos.adt ...
- Android(java)学习笔记203:JNI之NDK开发步骤
1. NDK开发步骤(回忆一下HelloWorld案例): (1)创建工程 (2)定义native方法 (3)创建jni文件夹 (4)创建c源文件放到jni文件夹 (5)拷贝jni.h头文件到jni目 ...
- js 或jquery定义方法时,参数不固定是怎么实现的
//①不定义接受参数的方式来接受参数(arguments) function getparams(){ //利用arguments来接受参数,arguments表示参数集合, //里面存放的调用这个方 ...
- mxnet.base.MXNetError: src/imperative/./imperative_utils.h:70: Check failed: inputs[i]->ctx().dev_mask() == ctx.dev_mask() (1 vs. 2)
mxnet 训练错误: mxnet.base.MXNetError: [14:42:22] src/imperative/./imperative_utils.h:70: Check failed: ...
- 笔试算法题(29):判断元素范围1到N的数组是否有重复数字 & 计算整数的7倍
出题:一个长度为N的数组,其中的元素取值范围是1到N,要求快速判断数组是否存在重复数字: 分析: 解法1:如果N个元素的范围都是在1到N,所以如果没有重复元素,则每一个位置恰好可以对应数组中的一个元素 ...
- buf.compare()
buf.compare(otherBuffer) otherBuffer {Buffer} 返回:{Number} 比较两个 Buffer 实例,无论 buf 在排序上靠前.靠后甚至与 otherBu ...
- Android使用JDBC连接数据库
连接数据库是安卓开发中几乎不可避免的一项工作,稍有规模的应用通常都需要使用数据库来存储用户数据.对于本地数据库当然可以使用sqlite,而对于多用户线上应用,则一般需要配备云端数据库.其中比较常用且开 ...
- 在移动端H5开发中(关于安卓端position:fixed和position:absolute;和虚拟键盘冲突的问题,以及解决方案)
一.在开发移动端webapp时,我们经常会遇到这样的问题,当我们需要在页面底部固定一个logo或者说明时,往往会采用position:fixed进行固定定位或者absolute定位到最底部 这是一个很 ...
- python之字典 2014-4-5
#字典:当索引不好用时1.字典 类似于php的关联数组 列表类似于索引数组 2.创建字典 phonebook={'alice':'2100','tom':'1900'} 键值之间用: 项之间用, 空字 ...
- Leetcode 143.重排链表
重排链表 给定一个单链表 L:L0→L1→…→Ln-1→Ln ,将其重新排列后变为: L0→Ln→L1→Ln-1→L2→Ln-2→… 你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换. 示 ...