cocos2dx实现自定义2D地形

先来看看效果:



对于2D地形的生成,可以采用2种方法,

1.使用建模软件将2D的地形模型构建好直接使用Sprite3D导入

 

优点:不需要太多程序控制,只是简单的导入

缺点:需要建模,而且还要转换成2D坐标,很难使用chipmunk加入物理特性

 

2.根据自定义数据使用顶点数据和shader。

下面来讲解如何实现,之后分析优缺点。

推荐一篇博客

如何制作一个类似Tiny Wings的游戏 Cocos2d-x 2.1.4

如果你看懂了上面的博客,那么就跟容易理解笔者讲解的。

 

首先准备,图片资源

    

                  草地(地形表面)                                             土地(地形底部)

地形数据,其实是由许多点连成线组成的,如图


只要获取这些点就可以得到一个比较平滑的地形,如果点的间隔比较大,得到的地形就没有那么平滑,所以两个点的水平距离要根据需要设置。



由于主要讲解如何根据点生成地形,这里就不讲解如何生成这么多点,有兴趣的同学可以加

QQ:842723654,一起讨论。


有了数据,那么应该如何生成地形呢?

我们都知道大多数3D模型是由许多点组成的(有些模型也包含曲线),这些点就组成了许多三角形,

这些三角形就组成了一个模型,给这个模型赋予贴图,就得到了一个完整的模型。

同样利用这个道理,我们可以自己构造一个2D模型。

对于上面由线段组成的曲线,如果将每一个点复制后向下平移一定的距离,就能得到


许多矩形(实际就多了一倍的顶点,目前还是点),现在就要把这些矩形拆分成三角形,

每个矩形沿对角线切割,就得到了三角形。


让我们想想opengl绘制三角形的方法,

glBegin(GL_TRIANGLES); ..... glEnd(); 这种方法又慢又费资源。

glBegin(GL_TRIANGLE_STRIP); ..... glEnd();  这种方法比刚才的快了一点

 

glDrawArrays(GL_TRIANGLE_STRIP,  ,  ); 这种一次绘制的就是我们想要的,

其实cocos2dx也是利用glDrawArrays来减少绘制次数的,尤其是对于粒子,一次性

传入所有粒子的顶点,一次性绘制,岂能不快。

这里也提一下,假如提供了1,2,3,4,5,6,6个顶点:

GL_TRIANGLES,每三个顶点构成一个三角形,(1,2,3),(4,5,6),绘制2个三角形

GL_TRIANGLE_STRIP,前面的两个顶点与当前顶点构成一个三角形,

(1,2,3),(2,3,4),(3,4,5),(4,5,6),绘制4个三角形

可见如果要节约资源,采用GL_TRIANGLE_STRIP是个不错的选择。

 

关于地形顶点数据的存储,笔者采用文件存储,前4个字节,表示顶点的个数,

以后每8个字节表示一个顶点,因为Vec2就占8个字节,正好可以读入。

如下,载入数据:

 

auto data = FileUtils::getInstance()->getDataFromFile(fileName);

CC_BREAK_IF(data.isNull());

 

int numPoint = *((int*)data.getBytes()); // 读取顶点个数

_points.resize(numPoint);

memcpy(&_points[0], data.getBytes() + 4, data.getSize() - 4); // 读取顶点数据

 

这里_points是std::vector<Vec2> _points;

vector的数据是连续的,而且Vec2又占8个字节,又不需要特殊的构造。


下一步就是将顶点数据转换成三角形顶点

对于土地部分


[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. bool TerrainBottom::initTriangle(std::vector<Vec2>& points)  
  2. {  
  3.     // triangles  
  4.     Vec2 pt(0.f, 0.f);  
  5.     for (std::size_t i=0; i< points.size(); ++i)  
  6.     {  
  7.         pt.x = points[i].x;  
  8.         _triangles.push_back(pt);       // 先下方顶点  
  9.         _triangles.push_back(points[i]);    // 再上方顶点  
  10.     }  
  11.     _bUserTexCoord = false;  // 是否使用自定义纹理坐标  
  12.   
  13.     return true;  
  14. }  

只要保证顶部地形的走势就可以,底部要做的就是一直拖到屏幕最下方,

所以根据上面标有数字的图片,按照数字顺序添加顶点就可以了。

 

而对于表面(草地)部分


实际上就是给土地镶嵌一层花边,如图中的黑色粗线


只要将每个顶点的Y坐标上下平移一小段距离,得到图中的橘色多边形就可以了

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. bool TerrainSurface::initTriangle(std::vector<Vec2>& points)  
  2. {  
  3.     // triangles  
  4.     Vec2 pt;  
  5.     for (std::size_t i=0; i< points.size(); ++i)  
  6.     {  
  7.         pt = points[i];  
  8.         _triangles.push_back(Vec2(pt.x, pt.y - 16.f));  // 向下平移  
  9.         _texCoords.push_back(Vec2(pt.x / 256.f, 0.f));  
  10.         _triangles.push_back(Vec2(pt.x, pt.y + 16.f));  // 向上平移  
  11.         _texCoords.push_back(Vec2(pt.x / 256.f, 1.f));  
  12.     }  
  13.     _bUserTexCoord = true;      // 是否使用自定义纹理坐标  
  14.     return true;  
  15. }  

这样土地和表面的三角顶点数据就构造好了

 

纹理的处理

纹理采用的是GL_REPEAT,对于地形的显示至关重要。

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. // texture  
  2. Texture2D::TexParams texParams;  
  3. texParams.magFilter = GL_LINEAR;  
  4. texParams.minFilter = GL_LINEAR;  
  5. texParams.wrapS = GL_REPEAT;  
  6. texParams.wrapT = GL_REPEAT;  
  7. tex->setTexParameters(texParams);  

那么纹理坐标应该怎么生成呢?

土地可以根据(坐标/图像的尺寸)得到。

表面水平方向/图像的宽带,竖直方向不能根据坐标,因为纹理是重复的,

只要将上顶点设为0.f,下顶点设为1.0f,就可以,纹理坐标Y坐标与opengl坐标系是相反的。

顶点坐标纹理都已准备好,下面就是如何绘制了.

Shader+绘制函数就可以搞定了,

继承Node,重载draw函数

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. void TerrainSprite::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)  
  2. {  
  3.     _customCmd.init(_globalZOrder, transform, flags);  
  4.     _customCmd.func = CC_CALLBACK_0(TerrainSprite::onDraw, this, transform, flags);  
  5.     renderer->addCommand(&_customCmd);  
  6. }  

定义一个CustomCommand,目的调用自己的绘制方法,

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. void TerrainSprite::onDraw(const Mat4 &transform, uint32_t flags)  
  2. {  
  3.     getGLProgramState()->apply(transform);  
  4.   
  5.     glDrawArrays(GL_TRIANGLE_STRIP, 0, _triangles.size());  
  6.     CC_INCREMENT_GL_DRAWN_BATCHES_AND_VERTICES(1, _triangles.size());  
  7. }  

我们并没有使用glVertexPointerglTexCoordPointer, 因为可以利用cocos2dx 的shader设置顶点属性

getGLProgramState()->setVertexAttribPointer,


[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. void setVertexAttribPointer(  
  2. const std::string &name, // 属性名 如attribute vec4 a_position  
  3. GLint size,              // 每个元素占几个type   
  4. GLenum type,             // 元素分量类型  如Vec2 占2个float type = GL_FLOAT  
  5. GLboolean normalized,    // 是否为法线?   
  6. GLsizei stride,          // 元素间隔 如 sizeof(Vec2)  
  7. GLvoid *pointer          // 元素指针  
  8. );  

可以这样提供给shader顶点坐标, 前提顶点着色器要有attribute vec4 a_position

getGLProgramState()->setVertexAttribPointer("a_position",2, GL_FLOATGL_FALSEsizeof(Vec2), &_triangles[0]);

同理纹理坐标

getGLProgramState()->setVertexAttribPointer("a_texCoord",2, GL_FLOATGL_FALSEsizeof(Vec2), &_texCoords[0])

可以参照tests的ShaderTests

到此我们已经知道地形绘制的方法,具体怎么实现呢?

组织方式

Terrain类管理地形的表面和土地

TerrainSurface类和TerrainBottom类分别显示地形的表面和土地

两者继承TerrainSprite类,TerrainSprite类显示自定义网格,

考虑一下Surface 和 Bottom 不同之处: 纹理,三角形数据,shader

仅仅几个参数不同,所以可以合并到TerrainSprite.

 

先来分析Terrain,载入地形数据,初始化表面和土地

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. lass Terrain : public Node  
  2. {  
  3. public:  
  4.     static Terrain* create(const std::string &fileName); // 根据文件生成地形  
  5.     bool init(const std::string &fileName); // 初始化  
  6.   
  7. private:  
  8.     bool loadTerrainData(const std::string &fileName); // 载入地形数据  
  9.   
  10. private:  
  11.     std::vector<Vec2> _points;  // 保存顶点数据  
  12.     TerrainSurface* _surface;    // 地形表面  
  13.     TerrainBottom* _bottom;    // 地形土地  
  14. };  

初始化代码:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. bool Terrain::init(const std::string &fileName)  
  2. {  
  3.     do   
  4.     {  
  5.         CC_BREAK_IF(!Node::init()); // 基类初始化  
  6.   
  7.         CC_BREAK_IF(!loadTerrainData(fileName)); // 载入数据  
  8.   
  9.         _bottom = TerrainBottom::create(_points); // 土地  
  10.         CC_BREAK_IF(_bottom == nullptr);  
  11.         this->addChild(_bottom);  
  12.   
  13.         _surface = TerrainSurface::create(_points); // 表面  
  14.         CC_BREAK_IF(_surface == nullptr);  
  15.         this->addChild(_surface);  
  16.   
  17.         return true;  
  18.     } while(0);  
  19.   
  20.     return false;  
  21. }  

然后TerrainSprite, 根据顶点数据构造三角顶点和纹理顶点,初始化shader,并绘制,头文件就不用看了,

由于三角形的处理不同所以提供了一个纯虚函数,让子类实现。

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. class TerrainSprite : public Node  
  2. {  
  3. public:  
  4.     static TerrainSprite* create(std::vector<Vec2>& points, Texture2D* tex, const std::string& vertFile, const std::string& fragFile);  
  5.     bool init(std::vector<Vec2>& points, Texture2D* tex, const std::string& vertFile, const std::string& fragFile);  
  6.       
  7.     void draw(Renderer *renderer, const Mat4 &transform, uint32_t flags);  
  8.     void onDraw(const Mat4 &transform, uint32_t flags);  
  9.   
  10. protected:  
  11.     virtual bool initTriangles(std::vector<Vec2>& points) = 0;  
  12.     bool initShader(Texture2D* tex, const std::string &vertFile, const std::string& fragFile);    
  13. protected:  
  14.     CustomCommand _customCmd;  
  15.     std::vector<Vec2> _triangles;  
  16.     std::vector<Vec2> _texCoords;  
  17.     bool _bUserTexCoord;  
  18. };  

先来看看初始化,初始化shader,顶点数据,设置参数

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. bool TerrainSprite::init(std::vector<Vec2>& points, Texture2D* tex, const std::string& vertFile, const std::string& fragFile)  
  2. {     
  3.     do   
  4.     {  
  5.         CC_BREAK_IF(!Node::init());  
  6.         CC_BREAK_IF(!initShader(tex, vertFile, fragFile));  
  7.         CC_BREAK_IF(!initTriangles(points));  
  8.           
  9.         getGLProgramState()->setVertexAttribPointer("a_position", 2, GL_FLOAT, GL_FALSE, sizeof(Vec2), &_triangles[0]);  // 顶点坐标  
  10.         if (_bUserTexCoord)  
  11.         {  
  12.             getGLProgramState()->setVertexAttribPointer("a_texCoord", 2, GL_FLOAT, GL_FALSE, sizeof(Vec2), &_texCoords[0]); // 纹理坐标  
  13.         }  
  14.   
  15.         return true;  
  16.     } while (0);  
  17.   
  18.     return false;  
  19. }  

再来看看shader的初始化, 改变纹理参数, 向shader设置纹理及尺寸

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. bool TerrainSprite::initShader(Texture2D* tex, const std::string &vertFile, const std::string& fragFile)  
  2. {  
  3.     // texture  
  4.     Texture2D::TexParams texParams;  
  5.     texParams.magFilter = GL_LINEAR;  
  6.     texParams.minFilter = GL_LINEAR;  
  7.     texParams.wrapS = GL_REPEAT;  
  8.     texParams.wrapT = GL_REPEAT;  
  9.     tex->setTexParameters(texParams);  
  10.   
  11.     // shader  
  12.     auto glProgram = GLProgram::createWithFilenames(vertFile, fragFile);  
  13.     auto glProgramState = GLProgramState::getOrCreateWithGLProgram(glProgram);  
  14.     glProgramState->setUniformTexture("u_terrain", tex);  
  15.         glProgramState->setUniformVec2("u_texSize", Vec2(tex->getContentSize().width, tex->getContentSize().height));  
  16.     setGLProgramState(glProgramState);  
  17.   
  18.     return true;  
  19. }  

最后分析TerrainSurface和TerrainBottom


[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. class TerrainSurface : public TerrainSprite  
  2. {  
  3. public:  
  4.     static TerrainSurface* create(std::vector<Vec2>& points);  
  5.   
  6. protected:  
  7.     virtual bool initTriangles(std::vector<Vec2>& points);  
  8.       
  9. };  

如何创建

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. TerrainSurface* TerrainSurface::create(std::vector<Vec2>& points)  
  2. {  
  3.     auto node = new (std::nothrow) TerrainSurface;  
  4.     if (node != nullptr)  
  5.     {  
  6.         auto tex = Director::getInstance()->getTextureCache()->addImage("surface.png");  
  7.         node->init(points,tex, "glsl/terrain_surface.vert""glsl/terrain_surface.frag");  
  8.         node->autorelease();  
  9.         return node;  
  10.     }  
  11.   
  12.     delete node;  
  13.     return nullptr;  
  14. }  

重点是这句

node->init(points,tex, "glsl/terrain_surface.vert""glsl/terrain_surface.frag");

纹理,shader,其实create还可以提供一个Texture2D*的参数以便得到不同的纹理。

initTriangles,在文章前面已经提到过,

TerrainBottom类似,具体请在文章最后下载源码查看。

Shader的编写

 

顶点着色器

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. attribute vec4 a_position;  
  2. attribute vec2 a_texCoord;  
  3.   
  4. #ifdef GL_ES  
  5. varying mediump vec2 v_texCoord;  
  6. #else  
  7. varying vec2 v_texCoord;  
  8. #endif  
  9.   
  10. uniform vec2 u_texSize; // 纹理尺寸,在bottom中用到  
  11.   
  12. void main()  
  13. {  
  14.     gl_Position = CC_MVPMatrix * a_position; // 一定记住是CC_MVPMatrix  
  15. v_texCoord = a_texCoord;  // 因为surface提供了纹理坐标  
  16. v_texCoord = vec2(a_position.x / u_texSize.x, a_position.y / u_texSize.y); // bottom需要根据顶点计算使用a_position而不使用gl_Position,a_position是应用程序传递的坐标,而gl_Position是计算后在模型视图的位置,试着替换为gl_Position。  
  17. }  

片元着色器

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. uniform sampler2D u_terrain;  
  2.   
  3. #ifdef GL_ES  
  4. varying mediump vec2 v_texCoord;  
  5. #else  
  6. varying vec2 v_texCoord;  
  7. #endif  
  8. void main()  
  9. {  
  10.     gl_FragColor = texture2D(u_terrain, v_texCoord);  
  11. }  

使用很简单

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. _terrain = Terrain::create("map.data");  
  2. this->addChild(_terrain);  

可见自定义数据,可以通过shader轻松更改显示特效,同时还可以添加物理特性.


源代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值