前段时间玩了《弹丸论破》,觉得游戏内那个指出矛盾时出现的玻璃破碎效果非常酷炫,然后嘛,作为一个资深但是却被某A打头大厂认为对cocos2dx完全不熟悉的菜逼,自然就产生了一个“我能不能用Cocos2dx也做一个碎裂化的效果呢?”
那今天小编带你们来尝试一下(吖屎吧,盗文章的傻逼们)
由于cocos2dx的精灵都是矩形的,所以为了讨论简单,这里我们用矩形的图来进行说明,方便大家理解。
另外,由于本blog是在完成了最初步骤后,顺着灵感写的,所以内容都还比较简单,因此目前设计的所有的碎片的切痕都是直线,毕竟砸碎一块方形的玻璃总不能出现个弧形的碎片吧(滑稽)。高端的的操作未来有空我再做。。
https://blog.youkuaiyun.com/u013962723/article/details/114117159
首先,我们需要制作碎片,想一下如果你将一张纸撕碎,注意是从头撕到尾,不是撕一个小猪佩奇,我们的碎片应该和我下图类似(当然这玻璃中间那一坨为啥不碎呢,举例而已,不要在意这些细节)
之前我有说过cocos2dx的精灵都是矩形,是因为纹理都是一张AXB的矩形,所以cocos2dx在渲染的时候,只需要用两个三角形拼成的矩形就能将一张纹理图片的内容进行呈现(你说为啥我们游戏里的图几乎都不是方的?朋友,听说过透明度的吧)。但这并没有说我们不能自己去定义需要绘制的形状。在实现翻折的文章里就能看到,需要渲染的形状是怎么样的,是根据你选取的顶点来决定的。
再看一下上面的图,有发现什么吗?没有?好吧。。。我们需要的碎片不都是一些多边形吗,那我们直接选取裂痕与裂痕间的交点(显然边界我们也可以认为是一道裂痕)作为顶点,构建出来的不就是表示碎片的多边形了。
碎片的定义与实现我们已经决定了,那现在需要做的是用程序实现出碎片。
才接触cocos2dx的小朋友可能会有一个定式,如果是一片颜色,那一定是LayerColor,如果是一个图,那一定是Sprite,如果是一个按钮,那一定是Button。但如果有看过源码,你会知道其实这些东西只是帮你整合了一些渲染,一些触碰逻辑,如果愿意,你甚至可以用Layer画出一个被触碰一下就叫一声的小猪佩奇。而由于精灵只是帮你绘制一个方形的纹理,所以如果要使用自定义顶点去渲染,那么精灵就不再使用了。
class Piece : public cocos2d::Node
Node是一个相对抽象的节点,但是却具备了所有我们需要用的接口,例如最重要的渲染。我们覆写Node中的虚方法draw。不过由于我们的碎片本质上其实还是渲染精灵,只是顶点不同而已,所以我们可以直接使用精灵的draw方法逻辑。
//Piece.h
virtual void draw(cocos2d::Renderer *renderer, const cocos2d::Mat4 &transform, uint32_t flags) override;
//Piece.cpp
void Piece::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
mConmmand.init(_globalZOrder,
mTexture,
getGLProgramState(),
_blendFunc,
mInfo.triangles,
transform,
flags);
renderer->addCommand(&mConmmand);
}
draw方法会将我们这个节点渲染配置加入到渲染器的渲染队列里,怎么渲染的我们不做研究,有兴趣的朋友可以自己去看看
这里渲染器,变换矩阵,flag(渲染器的)是外部参数
_globalZOrder是层级,mTexture是我们的纹理,getGLProgramState()是存储了我们这个节点使用的着色器状态(着色器也是使用精灵用的着色器),_blendFunc存储了混合模式,需要手动设置,一般选择用BlendFunc::ALPHA_PREMULTIPLIED方式, mInfo.triangles便是需要绘制的三角形。
层级与混合模式可以用精灵那种方式调用接口即可。纹理,着色器当然也可以,当然我这里是从父节点传进来。
class Piece : public cocos2d::Node
{
//https://blog.youkuaiyun.com/u013962723/article/details/114117159
public:
static Piece* createWithVertex(cocos2d::Texture2D *tex, std::vector<cocos2d::Vec2> points);
virtual bool initWithVertex(cocos2d::Texture2D *tex, std::vector<cocos2d::Vec2> points);
//...
};
最后就剩下绘制的三角形需要赋值了。
别的不说,其实也没啥好说的,看代码一目了然,甚至都没有太多可以装逼的。
void Piece::updateVertex()
{
std::vector< V3F_C4B_T2F> indices;
Rect rect(Vec2(0, 0), getContentSize());
for (auto iter = mPoints.begin(); iter != mPoints.end(); iter++)
{
Vec2 point = *iter;
indices.emplace_back(V3F_C4B_T2F{ Vec2ToVec3(point, 0), Color4B(0xff, 0xff, 0xff, 0xff), Vec2ToTex2(point) });
}
TrianglesCommand::Triangles triangles;
triangles.verts = new V3F_C4B_T2F[indices.size()];
for (int i = 0; i < indices.size(); i++)
{
indices[i].vertices.x *= rect.getMaxX();
indices[i].vertices.y *= rect.getMaxY();
triangles.verts[i] = indices[i];
}
triangles.vertCount = indices.size();
std::vector<unsigned short> vec;
for (int i = 1; i< indices.size() - 1; i++)
{
vec.emplace_back(0);
vec.emplace_back(i);
vec.emplace_back(i + 1);
}
triangles.indices = new unsigned short[vec.size()];
triangles.indexCount = 0;
for (auto iter = vec.begin(); iter != vec.end(); iter++)
{
unsigned short v = *iter;
triangles.indices[triangles.indexCount] = v;
triangles.indexCount++;
}
mInfo.setTriangles(triangles);
}
//https://blog.youkuaiyun.com/u013962723/article/details/114117159
和之前翻折的代码类似,只是这里少了一个背面纹理的处理,另外,三角形顶点的选用采取了固定一个顶点,然后依次取线段的做法。这段逻辑获得的三角形就是ABC,ACD,ADE。
需要特别注意的是,使用这种做法的前提,必须要多边形是一个凸多边形,你可以想象一下凹多边形如果单纯使用这种做法会是怎么样的。由于凹多边形的算法会复杂不少,这里暂不讨论(其实是因为我只有想法,但还没实现,以后的篇章会做的,另外网上肯定早就有大佬做出来了,毕竟这个算法其实也并不难)。
下面则是碎片的父节点的炸裂方法,这里的CrackMethod是一个结构体,存储了碎片的多边形的顶点,炸裂方向,与炸裂速度(主要目的还是做碎片,所以其他的都弄得很随意)
void FragmentNode::crack(std::vector<CrackMethod> methods)
{
for (auto iter = mPiecesVec.begin(); iter != mPiecesVec.end(); iter++)
{
auto piece = *iter;
piece->removeFromParent();
}
//https://blog.youkuaiyun.com/u013962723/article/details/114117159
for (auto iter = methods.begin(); iter != methods.end(); iter++)
{
auto method = *iter;
auto piece = Piece::createWithVertex(mTexture, method.points);
piece->setGLProgram(getGLProgram());
addChild(piece);
mPiecesVec.emplace_back(piece);
Vec2 direction = method.direction;
float speed = method.speed * 5;
if (!method.direction.isZero())
{
auto rep =
RepeatForever::create(
Sequence::create(
MoveBy::create(2.0, speed * direction),
MoveBy::create(2.0, speed * direction)->reverse(),
nullptr));
piece->runAction(rep);
}
}
}
可以看到,上述代码里并没有对碎片的位置进行设置,就直接被添加到了FragmentNode中。原因其实很简单,因为我将所有碎片与父节点的大小(ContentSize)都是一样的,皆为纹理的大小。而我们在绘制碎片的时候,是根据顶点对应纹理的百分比来绘制的。如下图,右下角的碎片的Node其实是有整个红色矩形那么大。所以这里既不需要设置锚点也不需要去设置位置,保证结点位置与父节点位置一致。
如此一来,我们将一张矩形的纹理肢解成若干个多边形碎片。
到此为止,我们的碎片便是实现了,但是这里面有几个说大不大但说小也不小的问题。
1.如果了解cocos3.x的合批原理的人都会知道合批这个优化概念。可以看到得是,我这里其实只有一张图片的纹理,但是我最初研究的时候,发现每一帧的drawcall是我的碎片数量。这显然不合理,于是我跟踪了一下渲染的代码才发现,我第一版的Piece在init的时候里,会装载一份着色器代码,因此5个碎片,就有5个代码,而要让cocos合批,就必须保证前后两次渲染的材质ID,也就是一个由着色器状态,纹理,混合参数共同合成的一个字段,而如果每一份都调用一份着色器装在的代码,那么这5个着色器得逻辑虽然是一摸一样的(因为是同样的代码),但也会被视为不同的着色器程序,因此导致状态不同,从而无法合批。
2.仔细看一下上面那张图的的切割处,你可以看到切割处明显的锯齿现象。如果我们需要去掉锯齿该怎么做呢?cocos2dx倒是为纹理类给出了抗锯齿的方法。
class CC_DLL Texture2D : public Ref
{
...
void setAntiAliasTexParameters(); // “抗锯齿”
void setAliasTexParameters(); //“不抗锯齿”
...
}
之所以我在这里打引号,是因为当你把纹理的抗锯齿打开,你会发现并没有用处,因为这个抗锯齿并非是多边形的抗锯齿,而是纹理的过滤方式,即用于对每个像素颜色的选择,让纹理更柔和。有兴趣的可以去看源码并且翻阅OpenGL的教程。
那我们应该怎么对多边形进行抗锯齿呢?OpenGL的教程里给出了一套做法
glEnable(GL_MULTISAMPLE); //激活多重采样
glfwWindowHint(GLFW_SAMPLES, 4);//多重采样样本数
引擎调用上述代码的地方可以自己搜索项目查看。
看完实现你会发现这么一个问题,多边形的抗锯齿对于cocos来说并不是对于纹理的,而是一个几乎是全局的设置。
具体做法如下代码
bool AppDelegate::applicationDidFinishLaunching() {
// initialize director
auto director = Director::getInstance();
auto glview = director->getOpenGLView();
auto attr = glview->getGLContextAttrs();
attr.multisamplingCount = 4;
glview->setGLContextAttrs(attr);
if(!glview) {
#if (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32) || (CC_TARGET_PLATFORM == CC_PLATFORM_MAC) || (CC_TARGET_PLATFORM == CC_PLATFORM_LINUX)
glview = GLViewImpl::createWithRect("Dragger", cocos2d::Rect(0, 0, designResolutionSize.width, designResolutionSize.height));
#else
glview = GLViewImpl::create("Dragger");
#endif
director->setOpenGLView(glview);
}
...
}
在项目的AppDelegate的创建窗口之前,需要提前对窗口参数进行设置,将multisamplingCount 这个值,也就是多重采样的数量进行设置,默认这个值是0,也就是不开启,也就不会抗锯齿。
仔细对比下图于上图的切口,显然下图的纹理的锯齿现象好多了。
3.其实在做这个效果的最开始,我是将所有碎片的都放在整个FragmentNode中的,但是我发现了如果这么做,我不太好处理碎片的位置变更。而如果分离成多个Node,那么就可以利用cocos2dx现成的动画系统,以及相应的变换工具。也就是说我可以把我们的碎片用精灵的方式“飞”出去。不过对于形变可能会出现一些奇怪的变形,因为之前也说了,我们的Node是整个红色框那么大。这里之后我会试图看一下能不能去适配位置与大小,让碎片的size和精灵一样,所见即所得。
4.由于Texture不是一个Node,不会被addChild,所以用的时候记得retain与release,避免内存泄漏。
https://blog.youkuaiyun.com/u013962723/article/details/114117159
其实写了这么多,这种做法用于生产的意义其实并不大,因为真想实现玻璃碎裂效果,不说3D引擎套3D建模有多容易,就是cocos2dx,请美术做一个2D的spine动画也并不会费多少事,而且效果还好看。不过话也不能说死,因为这种做法可以自定义任意的纹理,任意的碎片。弹丸破碎那种纹理碎屏的方式用这种方法来做,显然开销更小,更灵活(至于好看嘛,这不是第一步嘛,慢慢来)。所以吧,有兴趣的朋友看看就好了,这种blog主要还是写给自己的。
那以上便是cocos2dx实现碎片的第一部分,之后部分有空再慢慢补了,毕竟这不是啥高端技术,纯粹是写下来自己看的。(营销号,写了这些话,我看你们怎么好意思盗文章还收费)