【目标】:将 ShatteredSprite 移植到 cocos2d-x 2.0.4 版本
【参考】:
superraccoon大神的版本
OpenGL ES for Android研究总结
一、引子
一直想要做一个爆炸的特效,尤其是看到了superraccoon的那个效果。理想中的效果包括两个部分:粒子特效构成的爆炸火焰,和网格特效构成的碎片效果。其中粒子特效我在 http://blog.csdn.net/ronintao/article/details/9899479 里面讨论过了,自信是能做出来的(实际上在那个夭折的飞机小游戏里面做了一个)。不过碎片效果还没有实现。
在superraccoon那个网址里面,提供了一个老版本的 ShatteredSprite,可以实现一个爆炸的碎片效果,不过那个版本在我的 cocos2dx 2.0版本上并不能直接正常运行。【现在分析下来主要差异在于 opengl es 2.0的差异,以及 ccpoint 数据结构不能正常工作导致。】
一气之下干脆自己花了两个晚上根据原理重新写了一个。这里讨论一下他的实现原理。
二、原理
其实 ShatteredSprite的原理很简单。首先将整个纹理分割为大量三角形,然后当开始爆炸时,每个三角形以不同的速度和旋转角度飞出去,就形成了爆炸的效果。
明确了原理,下一步明确我们应该如何实现:首先要将纹理分割为大量三角形,然后考虑如何将这些三角形进行旋转和位移。
三、网格化并通过 drawArray显示
既然是要分割成三角形,那么最终是要通过glDrawArray来绘制大量三角形,拼接成一个完整纹理。老实说,由于没用过ES2.0,开始的时候完全没有概念。这个时候 cocos的源码是我们最好的老师,这里可以参考的是 ccsprite的 draw函数,由于比较简单,这里就不列出了,简而言之就是调用glVertexAttribPointer设置顶点,然后调用glDrawArrays绘制。
拿到一个纹理之后,我们要进行分割。我这里写死为分割为横8块,竖8块,然后每个方格又切成两个三角形,总共128块。
其坐标关系如上图所示,一个像上面位置的方块,我们称其位置为(x, y),那么左下角就是(0, 0),如果将二维坐标数组转为一维,那么按照先算第一竖列,再算第二竖列的顺序来算,那么就是第 x*Y + y个方块,其中Y是一列的高度。
那么再进行三角形的切割,就可以知道两个三角形分别是第 2(x*Y + y) 和 第 2(x* Y +y) + 1个三角形,这两个三角形的坐标(注意逆时针)分别是:
第2(x*Y + y) 个三角形:{ 左下角(x, y+1)、右下角(x+1, y)、左上角(x, y+1) }
第2(x* Y +y) + 1个三角形:{ 右上角(x+1, y+1)、 左上角(x, y+1)、右下角(x+1, y)}
这样知道了整个图片的高和宽,顶点的分布就可以确定了,纹理坐标也对应出来了,需要注意的是,纹理坐标的UV坐标系范围是从0到1,然后是MM_TEXT坐标系,正方向是右下。
至于颜色数组该如何分配,其实我没有完全搞清。目前已知的情况是如果不画,最终出不来图像,但是只需要制定为默认的全255好像就可以了。
对应的代码如下,首先在初始化的时候初始化三个数组:
void RoninShatterSprite::shatterSprite(CCSprite *sprite, float speedVar, float rotVar) { initWithTexture(sprite->getTexture()); //init vertices array const int X_PIECE = 8; const int Y_PIECE = 8; float xVerLength = WIDTH(sprite) / X_PIECE; float yVerLength = HEIGHT(sprite) / Y_PIECE; //生成一个point数组,省掉后面计算,由于边界的存在,比分出的份要多一 CCPoint ptArray[ X_PIECE + 1 ][ Y_PIECE + 1 ]; CCPoint texArray[ X_PIECE + 1 ][ Y_PIECE + 1]; for ( int x = 0; x <= X_PIECE; x ++ ) { for ( int y = 0; y <= Y_PIECE; y ++ ) { ptArray[x][y] = ccp( xVerLength * x, yVerLength * y ); //注意TEXTURE是 MM_TEXT 坐标系,可以参考 http://blog.csdn.net/taibushuang/article/details/6435390 texArray[x][y] = ccp( ptArray[x][y].x / WIDTH(sprite), 1.0f - ptArray[x][y].y/HEIGHT(sprite) ); } } for ( int x = 0; x < X_PIECE; x ++ ) { for ( int y = 0; y < Y_PIECE; y ++ ) { mVertices[ (x*Y_PIECE + y) * 2 ] = triVer( ptArray[x][y], ptArray[x+1][y], ptArray[x][y+1] ); mTexcords[ (x*Y_PIECE + y) * 2 ] = triTex( texArray[x][y], texArray[x+1][y], texArray[x][y+1] ); mVertices[ (x*Y_PIECE + y) * 2 + 1 ] = triVer( ptArray[x+1][y+1], ptArray[x][y+1], ptArray[x+1][y] ); mTexcords[ (x*Y_PIECE + y) * 2 + 1 ] = triTex( texArray[x+1][y+1], texArray[x][y+1], texArray[x+1][y] ); } } //init color array, fill with 255 ccColor4B tmpColor = { 255, 255, 255, 255 }; for ( int i = 0; i < MAX_VERTEX; i ++ ) mClrArray[i] = triClr(tmpColor, tmpColor, tmpColor); }然后再绘制的时候,绘制这三个数组:
void RoninShatterSprite::draw() { //CCSprite::draw(); CC_NODE_DRAW_SETUP(); ccGLBlendFunc(m_sBlendFunc.src, m_sBlendFunc.dst); ccGLBindTexture2D(getTexture()->getName()); //init时set进去的 ccGLEnableVertexAttribs( kCCVertexAttribFlag_PosColorTex ); glVertexAttribPointer(kCCVertexAttrib_Position, 2, GL_FLOAT, GL_FALSE, 0, mVertices); glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE, 0, mTexcords); glVertexAttribPointer(kCCVertexAttrib_Color, 4, GL_UNSIGNED_BYTE, GL_TRUE, 0, mClrArray); glDrawArrays(GL_TRIANGLES, 0, 3 * MAX_VERTEX); }就可以看到图像了。
这里需要特别说明的就是数据结构,原来的ShatteredSprite是用CCPoint来做的,确实由于cocos提供了很多辅助宏和方法,用起来很方便,但是在我的机器上跑,始终表现不正常,说明可移植性并不好。最好的方法还是老老实实的用 ccVertex2F 来构成,这里也贴一下我的数据结构:
//数据结构 struct RoninTriVer { cocos2d::ccVertex2F vertic1; cocos2d::ccVertex2F vertic2; cocos2d::ccVertex2F vertic3; }; struct RoninTriTex { cocos2d::ccTex2F tex1; cocos2d::ccTex2F tex2; cocos2d::ccTex2F tex3; }; struct RoninTriClr { cocos2d::ccColor4B color1; cocos2d::ccColor4B color2; cocos2d::ccColor4B color3; };
四、让碎片飞
完成上面的一步,其实就已经完成了50%。下面我们要实现碎片乱飞的效果。从老版本的ShatteredSprite很容易看出来是怎么做的:开始的时候为每一个三角形生成一个速度和旋转量,然后在每次update的时候根据这两个数值,来进行位移和旋转。
由于非常简单,所以我这里直接贴代码,需要说明的是,原来的 randf 方法在我这里运行貌似也有问题,我没有具体调试,直接替换成了cocos的CCRANDOM_MINUS1_1:
首先在初始化的时候,为每个三角形碎片生成一个速度分量和旋转分量:
void RoninShatterSprite::shatterSprite(CCSprite *sprite, float speedVar, float rotVar) { ..............//前面不变 for ( int x = 0; x < X_PIECE; x ++ ) { for ( int y = 0; y < Y_PIECE; y ++ ) { mVertices[ (x*Y_PIECE + y) * 2 ] = triVer( ptArray[x][y], ptArray[x+1][y], ptArray[x][y+1] ); mTexcords[ (x*Y_PIECE + y) * 2 ] = triTex( texArray[x][y], texArray[x+1][y], texArray[x][y+1] ); //为第 (x*Y_PIECE + y) * 2 个三角形生成速度和旋转分量 velocityArray[ (x*Y_PIECE + y) * 2 ] = ccp( CCRANDOM_MINUS1_1() * speedVar, CCRANDOM_MINUS1_1() * speedVar ); rotationArray[ (x*Y_PIECE + y) * 2 ] = CCRANDOM_MINUS1_1() * rotVar; mVertices[ (x*Y_PIECE + y) * 2 + 1 ] = triVer( ptArray[x+1][y+1], ptArray[x][y+1], ptArray[x+1][y] ); mTexcords[ (x*Y_PIECE + y) * 2 + 1 ] = triTex( texArray[x+1][y+1], texArray[x][y+1], texArray[x+1][y] ); //为第 (x*Y_PIECE + y) * 2 + 1个三角形生成速度和旋转分量 velocityArray[ (x*Y_PIECE + y) * 2 + 1 ] = ccp( CCRANDOM_MINUS1_1() * speedVar, CCRANDOM_MINUS1_1() * speedVar ); rotationArray[ (x*Y_PIECE + y) * 2 ] = CCRANDOM_MINUS1_1() * rotVar; } } .......//后面也不变 }然后再update的时候,根据速度移动三角形的三个角,然后以三角形的重心为中心进行旋转,注意只要移动顶点数据就可以,纹理是不用变化的,毕竟这个三角形中的内容没有发生变化:
void RoninShatterSprite::update(float dt) { for ( int i = 0; i < MAX_VERTEX; i ++ ) { mVertices[i].vertic1 = vertex2( mVertices[i].vertic1.x + velocityArray[i].x, mVertices[i].vertic1.y + velocityArray[i].y ); mVertices[i].vertic2 = vertex2( mVertices[i].vertic2.x + velocityArray[i].x, mVertices[i].vertic2.y + velocityArray[i].y ); mVertices[i].vertic3 = vertex2( mVertices[i].vertic3.x + velocityArray[i].x, mVertices[i].vertic3.y + velocityArray[i].y ); CCPoint center = ccp( (mVertices[i].vertic1.x + mVertices[i].vertic2.x + mVertices[i].vertic3.x)/3, (mVertices[i].vertic1.y + mVertices[i].vertic2.y + mVertices[i].vertic3.y)/3); CCPoint rotatedV1 = ccpRotateByAngle( ccp(mVertices[i].vertic1.x, mVertices[i].vertic1.y), center, rotationArray[i] ); CCPoint rotatedV2 = ccpRotateByAngle( ccp(mVertices[i].vertic2.x, mVertices[i].vertic2.y), center, rotationArray[i] ); CCPoint rotatedV3 = ccpRotateByAngle( ccp(mVertices[i].vertic3.x, mVertices[i].vertic3.y), center, rotationArray[i] ); mVertices[i].vertic1 = vertex2( rotatedV1.x, rotatedV1.y ); mVertices[i].vertic2 = vertex2( rotatedV2.x, rotatedV2.y ); mVertices[i].vertic3 = vertex2( rotatedV3.x, rotatedV3.y ); } }好了,现在你的shatterSprite已经可以碎片乱飞了~~~~
五、随机的碎片大小
注意到我们现在每个碎片的大小都是一样的,可以再把这个碎片的大小随机化一点,这个是直接抄老的 shatterSprite 的【其实连思想都是抄的,我只是研究了一下而已】。
在初始化原来生成ptArray的地方稍微修改一下即可:
CCPoint ptArray[ X_PIECE + 1 ][ Y_PIECE + 1 ]; CCPoint texArray[ X_PIECE + 1 ][ Y_PIECE + 1]; for ( int x = 0; x <= X_PIECE; x ++ ) { for ( int y = 0; y <= Y_PIECE; y ++ ) { ptArray[x][y] = ccp( xVerLength * x, yVerLength * y ); if ( x > 0 && x < X_PIECE && y > 0 && y < Y_PIECE ) ptArray[x][y] = ccpAdd( ptArray[x][y], ccp(CCRANDOM_MINUS1_1() * xVerLength * 0.45, CCRANDOM_MINUS1_1() * yVerLength * 0.45 ) ); //注意TEXTURE是 MM_TEXT 坐标系,可以参考 http://blog.csdn.net/taibushuang/article/details/6435390 texArray[x][y] = ccp( ptArray[x][y].x / WIDTH(sprite), 1.0f - ptArray[x][y].y/HEIGHT(sprite) ); //ccp( xTexLength * x, yTexLength * (Y_PIECE - y) ); } }
六、炸过来、炸回去
为了给同学演示而加的一个额外的小功能,可以再把炸开的碎片收回来。如果你已经理解了前面的流程,这里想必可以立刻明白:只要把原来做的位移和旋转操作再反过来做一遍即可。
我这里用一个 isBoom来记录状态,确定到底是在炸开的状态还是收缩的状态,然后用一个 boomFrameCount 记录炸开动作的帧数,当需要炸回来的时候,再反向操作boomFrameCount帧就可以回到原始状态。
当然,由于浮点操作的误差,多次这样炸过来炸过去,还是会出现一定误差, 表现是图像上出现三角形间的小缝隙。解决的方法是把最原始的位置记录一下,在炸回去的最后一帧,做一个彻底恢复的动作。
具体代码如下:
void RoninShatterSprite::update(float dt) { //如果已经炸回原形 if ( !isBoom && boomFrameCount <= 0 ) { restore(); unscheduleUpdate(); return; } int boomFlag = isBoom ? 1 : -1; boomFrameCount += boomFlag; for ( int i = 0; i < MAX_VERTEX; i ++ ) { mVertices[i].vertic1 = vertex2( mVertices[i].vertic1.x + boomFlag * velocityArray[i].x, mVertices[i].vertic1.y + boomFlag * velocityArray[i].y ); mVertices[i].vertic2 = vertex2( mVertices[i].vertic2.x + boomFlag * velocityArray[i].x, mVertices[i].vertic2.y + boomFlag * velocityArray[i].y ); mVertices[i].vertic3 = vertex2( mVertices[i].vertic3.x + boomFlag * velocityArray[i].x, mVertices[i].vertic3.y + boomFlag * velocityArray[i].y ); CCPoint center = ccp( (mVertices[i].vertic1.x + mVertices[i].vertic2.x + mVertices[i].vertic3.x)/3, (mVertices[i].vertic1.y + mVertices[i].vertic2.y + mVertices[i].vertic3.y)/3); CCPoint rotatedV1 = ccpRotateByAngle( ccp(mVertices[i].vertic1.x, mVertices[i].vertic1.y), center, boomFlag * rotationArray[i] ); CCPoint rotatedV2 = ccpRotateByAngle( ccp(mVertices[i].vertic2.x, mVertices[i].vertic2.y), center, boomFlag * rotationArray[i] ); CCPoint rotatedV3 = ccpRotateByAngle( ccp(mVertices[i].vertic3.x, mVertices[i].vertic3.y), center, boomFlag * rotationArray[i] ); mVertices[i].vertic1 = vertex2( rotatedV1.x, rotatedV1.y ); mVertices[i].vertic2 = vertex2( rotatedV2.x, rotatedV2.y ); mVertices[i].vertic3 = vertex2( rotatedV3.x, rotatedV3.y ); } }恢复的动作,很简单:
void RoninShatterSprite::restore() { boomFrameCount = 0; for ( int i = 0; i < MAX_VERTEX; i ++ ) mVertices[i] = mOriginVertices[i]; }
简易版cocos2dx 2.0版本 ShatterSprite,可以实现sprite的炸成碎片的效果(以及恢复到原体的效果)。自带一个简单的DEMO(当然你要先配环境才能运行),按下屏幕开始爆炸,手指离开屏幕开始恢复。
地址:http://download.csdn.net/detail/ronintao/6548381