使用cocos2dx实现碎片炸裂 P3 (翻面与双面纹理)

在前两节中,我们已经可以将一块儿纹理肢解成很多个多边形的方式来实现碎片,但我们的纹理现在只能实现平移,显然正常来说碎片炸裂开,除了水平上的位移以外,最起码也还应该有一个朝某一个方向的自转吧。通常来说,我们可以用setScale的方式,来表现一个2D图形,绕着平行于所在平明的任意一条直线的一个翻转(当然对于进阶一点的开发人员,会使用OrbitCamera)。不过有一个很关键的问题,翻一圈以后,你的纹理并没有变化,而还是正面的纹理,这就显得不太合适了吧。

所以这一节,我将分享一下如何使用cocos2dx来实现双面纹理。

其实,我在之前的翻折里,做过类似的事,一般的朋友也会想到那种做法。保存两份纹理,遇到是背面的时候直接用背面的纹理不就好了。相信不少朋友都做过扑克牌翻拍,或者计时牌翻牌那种。只需要对牌执行一个序列动画,翻转,调用换图,继续翻转。但有没有想过,我们的碎片翻转速度不一致,运动轨迹不一致,而且翻转方向可能也会不一致,翻转圈数也不是只有一次,这些信息加起来可就比简单的翻牌序列动画复杂太多了。

另外补充一下,cocos2dx底层渲染是使用了OpenGL的。我们需要运用到openGL的一些功能来辅助我们实现这个效果。

一个3D游戏开发(不知道3D开发会不会涉及到这个,不过我觉得怎么也该吧),或者至少学习了一些OpenGL皮毛的开发人员,肯定对OpenGL中的“面剔除”这个功能有点印象吧,在这一部分中,我们会知道一个gl_FrontFacing参数,这个参数表示当前的图元是否为正面,至于什么是正面,可以去https://learnopengl-cn.github.io/04%20Advanced%20OpenGL/04%20Face%20culling/看一看。简单读一下就会发现,这与我们在前一节中对于多边形的定义方式很像,这也是为了引入这一节中关于面剔除相关内容做的准备。

既然有了这么一个开关,着色器直接就可以进行正反面的的判断,而不需要我们来对纹理进行正反切换了。

以下便是片段着色器的代码。

#ifdef GL_ES
precision lowp float;
#endif

varying vec4 v_fragmentColor;
varying vec2 v_texCoord;

void main()
{
	if (gl_FrontFacing)
	{
		gl_FragColor = v_fragmentColor * texture2D(CC_Texture0, v_texCoord);
	}
	else
	{
		gl_FragColor = v_fragmentColor * texture2D(CC_Texture1, v_texCoord);
	}
}
/* 常规的着色器
void main()
{
	gl_FragColor = v_fragmentColor * texture2D(CC_Texture0, v_texCoord);
}
*/

相比之前的着色器,只是一个分支判断,如果是正面则原始纹理,否则则使用第二个纹理。看起来非常简单。

 

但是,但是,但是,我们这里是使用cocos2dx来制作这个功能,而不是U3D或者UE4,3D引擎的纹理管理我不确定是否有现成的控件或者方案完成,但是对于cocos2dx来说,至少我这么多年的经验里是从来没有遇到过的。(当然,也是因为我太菜了,毕竟A打头大厂已经讲明我对cocos2dx一点都不了解了)

而且cocos2dx毕竟是一个2D引擎,不会特意为开发人员准备一个具有背面纹理的Sprite。

 

为了能够一次性使用两份纹理,我再一次对引擎源码进行了研读。

https://blog.csdn.net/u013962723/article/details/114405849

以下内容基于cocos2dx 3.17.2版本,很有可能,我提及的内容是后续版本新增的特性,低版本引擎没有相关逻辑,所以文章请结合手上引擎的版本观看,毕竟这里只是一个分享,不是说明文档。

 

对openGL有入门级了解的人会知道,为openGL绑定纹理的接口是glBindTexture,而里面的参数则是着色器中的纹理ID,以及需要绑定的纹理。对整个引擎搜索glBindTexture的时候你会发现还是有不少用到的地方。

跳转过去后,会看到如下代码。

void bindTexture2DN(GLuint textureUnit, GLuint textureId)
{
#if CC_ENABLE_GL_STATE_CACHE
	CCASSERT(textureUnit < MAX_ACTIVE_TEXTURE, "textureUnit is too big");
	if (s_currentBoundTexture[textureUnit] != textureId)
	{
		s_currentBoundTexture[textureUnit] = textureId;
		activeTexture(GL_TEXTURE0 + textureUnit);
		glBindTexture(GL_TEXTURE_2D, textureId);
	}
#else
	glActiveTexture(GL_TEXTURE0 + textureUnit);
	glBindTexture(GL_TEXTURE_2D, textureId);
#endif
}

也就是说,cocos2dx确实已经帮我们提供了接口去调用openGL绑定纹理的逻辑。

既然如此,我们的思路就非常明确了。调用上述接口绑定背面纹理,然后使用我们上面自定义的片段着色器。看起来EasyEasy啦………………个鬼……

 

我们之前在进行着色的时候一直是使用的是TriangleCommand,这个的好处是我们只需要传入顶点数据,cocos会帮我们把合批,顶点绑定,纹理使用等等一系列渲染需要使用的逻辑都一起做了,但正因为所有事情都帮我们做了,因此除非直接修改TriangleCommand的实现(准确来说是Renderer中解析TriangleCommand的实现),我们是没有办法在渲染的时候绑定背面纹理的。

当然,我们也可以使用CustomCommand来做,如下

void Piece::draw(cocos2d::Renderer *renderer, const cocos2d::Mat4 &transform, uint32_t flags)
{
	mCustomCommand.init(_globalZOrder, transform, flags);
	mCustomCommand.func = CC_CALLBACK_0(Piece::onDraw, this, transform, flags);
	renderer->addCommand(&mCustomCommand);
}

void Piece::onDraw(const cocos2d::Mat4 &parentTransform, uint32_t parentFlags)
{
// Bind our texture
//	Draw our pieces
}

但这样写的后果是我们将无法直接进行合批。这就意味着,我们有几个碎片,drawcall就会调用多少次。但这里的纹理都是一样的,甚至可以直接拼起来成为一张完整的图,显然应该被合在一次drawcall中完成。

https://blog.csdn.net/u013962723/article/details/114405849

毛主席说过,解决问题要找主要矛盾。现在矛盾非常清楚,自定义命令无法直接进行合批,三角命令不改源码无法绑定纹理。这两个矛盾只要能处理一个,我们就能高效地实现双面纹理的渲染了。

那首先,我考虑将自定义命令合批。从逻辑上来看,这个是可以做到的,这里的渲染除了使用了第二个纹理缓存外,与三角命令没有任何区别。那最容易想到的思路就是仿照三角命令合批的逻辑,自己写一份。事实上,实现合批的方法其实非常简单,就是将相同纹理,相同着色器,相同混合方式(在cocos2d中,这三者被合并,称为相同材质material),的精灵的顶点合在一起,然后一次性全部绘制出来。

因此我们的思路其实很简单,收集所有碎片的顶点,整合到一块儿draw出来。但这么做需要解决的一个问题是,我们没有办法知道当前之后是否还有更多的碎片需要被我们合批。

不过只要思想不滑坡,办法总比困难多。处理这个的方法并不是没有,我使用了一个全局的单例,来存储这种渲染的顶点,当我们需要绘制碎片时,我们不再进行绘制,而是把顶点收集到单例中,然后在所有碎片都完成收集的时间点,再进行渲染。

需要做到这种操作,需要顺便再聊一聊cocos2dx的渲染过程。

cocos2dx在一次循环中,会先进行定时器以及动画的update,更新这一帧的信息,然后从根节点(我们的scene),开始进行递归visit,用以收集与处理顶点信息,然后再从根节点进行递归draw,将渲染信息加入到渲染器队列,最后渲染器将渲染命令以此执行,进行渲染(这个应该是3.X的做法,2.X好像并没有使用渲染队列)。通常来说只有在visit完成之后,引擎才会进行draw。因此我们只需要再visit时进行信息收集,而在父节点中去draw就可以保证至少这个节点下的碎片能被合批了。

void Piece::visit(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
	Node::visit(renderer, transform, flags);
//    PieceBatchController::getInstance()->addVertical()
//https://blog.csdn.net/u013962723/article/details/114405849
}

void FragmentNode::draw(cocos2d::Renderer *renderer, const cocos2d::Mat4 &transform, uint32_t flags)
{
	mCustomCommand.init(_globalZOrder, transform, flags);
	mCustomCommand.func = CC_CALLBACK_0(FragmentNode::onDraw, this, transform, flags);
	renderer->addCommand(&mCustomCommand);
}

void FragmentNode::onDraw(const cocos2d::Mat4 &parentTransform, uint32_t parentFlags)
{
//	PieceBatchController::getInstance()->draw();
}

但是,在我经历过一下午的尝试后,我放弃了,因为写起来实在很蛋疼……因为需要改动的编写的地方实在太多了,相当麻烦(我反正弄了一两个小时,shi都改出来了……)。当然,如果有兴趣想自己尝试也不是不行。

 

既然第一种方式放弃了,那就试着用一下第二种方式。

于是,我再次查看了TriangleCommand进行渲染的部分,在查看使用纹理的地方,可以看到如下代码。

void Renderer::drawBatchedTriangles()
{
//many code
    /************** 3: Draw *************/
    for (int i=0; i<batchesTotal; ++i)
    {
        CC_ASSERT(_triBatchesToDraw[i].cmd && "Invalid batch");
        _triBatchesToDraw[i].cmd->useMaterial();
        glDrawElements(GL_TRIANGLES, (GLsizei) _triBatchesToDraw[i].indicesToDraw, GL_UNSIGNED_SHORT, (GLvoid*) (_triBatchesToDraw[i].offset*sizeof(_indices[0])) );
        _drawnBatches++;
        _drawnVertices += _triBatchesToDraw[i].indicesToDraw;
    }

//more code ...
}

useMaterial,使用材质,这个一看就是绑定纹理的地方,所以我们看一下里面的实现。

void TrianglesCommand::useMaterial() const
{
    //Set texture
    GL::bindTexture2D(_textureID);
    
    if (_alphaTextureID > 0)
    { // ANDROID ETC1 ALPHA supports.
        GL::bindTexture2DN(1, _alphaTextureID);
    }
    //set blend mode
    GL::blendFunc(_blendType.src, _blendType.dst);
    
    _glProgramState->apply(_mv);
}

然后,就发现大宝贝了。原来,cocos2dx(请注意,我使用的是3.17.2版本)使用了第二套纹理。只需要_alphaTextureID大于0就好了,而_alphaTextureID是在初始化的地方赋值的。

void TrianglesCommand::init(float globalOrder, Texture2D* texture, GLProgramState* glProgramState, BlendFunc blendType, const Triangles& triangles, const Mat4& mv, uint32_t flags)
{
    init(globalOrder, texture->getName(), glProgramState, blendType, triangles, mv, flags);
    _alphaTextureID = texture->getAlphaTextureName();
}

只是看命名,我相信大家也能猜个八九不离十,这个东西是被存储在Texture2D中的一个参数。Texture2D除了本身纹理以外的透明度纹理。所以只需要我们将我们的纹理,设置了透明度的纹理,就可以用到了。

https://blog.csdn.net/u013962723/article/details/114405849

而至于怎么用,虽然命名写的是Alpha,但谁说就一定要用透明度呢?事实上,透明度纹理的用法显然是需要套用到特定的着色器,简言之就是让RGB部分使用前纹理,而透明度部分使用透明度纹理。但我们的着色器并没有这么写,所以名字只是一个代号,就不要太过于看这些细节了。

bool FragmentNode::initWithFile(const char* filePath, const char* backFilePath)
{
	if (!Node::init())
	{
		return false;
	}
//https://blog.csdn.net/u013962723/article/details/114405849
	setIgnoreAnchorPointForPosition(false);
	setAnchorPoint(Vec2(0.5, 0.5));

	mTexture = Director::getInstance()->getTextureCache()->addImage(filePath);
	mTexture->retain();
	
	if (backFilePath != nullptr)
	{
		auto backTexture = Director::getInstance()->getTextureCache()->addImage(backFilePath);
		mTexture->setAlphaTexture(backTexture);
	}
	setContentSize(mTexture->getContentSize());

	GLProgram* gl = GLProgram::createWithFilenames("shader/standard.vert", "shader/standard_back.frag");
	setGLProgram(gl);

	return true;
}

于是我们只需要这么加一点,就可以支持双面纹理了。

怎么样,虽然还是很挫,但至少看起来更像碎片炸开了吧。(3D大佬们就当笑话看看好了,毕竟这些东西3D引擎做起来就跟喝水一样写意)

另外drawCall也只有一次,虽然我们的纹理是两张图。

本文并不完整,如果有兴趣自己实现了一下会发现,翻转效果会和预想完全不同(但由于另一部分内容太多了,所以我准备拆成2部分来写)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值