GLSL Shader特效教程4:Render2Texture之FBO

2015年03月19日 10:40 0 点赞 0 评论 更新于 2025-11-21 17:38

在实际开发中,我们常常需要对场景或场景中的部分OBJ进行特殊处理,而且这种处理往往是多方面的,一次处理可能无法满足需求。例如,需要对物体A先进行高斯模糊处理,再进行边缘检测;或者对场景中的多个物体A、B、C等同时施加特效。当需要对一个OBJ进行两次以上处理时,就会遇到问题。因为OBJ完成第一次处理后,结果会直接输出到窗口的缓冲区,我们无法获取第一次处理的结果,也就无法进行第二次处理后再输出到显示缓冲区并显示在屏幕上。

问题原因及FBO技术的引入

这种情况的出现是因为操作系统(OS)的显示缓冲区不受OpenGL(GL)控制,即GL无法干涉和控制OS提供的缓冲区。为了解决这个问题,GL引入了FBO(Frame Buffer Object)技术。与OS提供的帧缓冲区不同,FBO完全受GL控制,这便是“Render to Texture”的来源。其含义是将渲染结果存储到受GL控制的FBO中,从某种意义上说,这个FBO可以被视为一个纹理(Texture)。该纹理可以直接输出到OS提供的帧缓冲区,像图片一样显示在OS的窗口上。

FBO技术的应用场景

对单个OBJ进行多次处理

当需要对一个OBJ进行两次以上处理时,FBO技术可以帮助我们获取每次处理的中间结果,从而实现多次处理。

对场景中多个物体进行处理

例如,场景中有A、B、C……H多个物体,需要对这些物体同时进行Shader特效或变换。一种直接的方法是每次都遍历所有需要处理的OBJ,逐个进行处理。但随着OBJ数量的增加,这种方法的效率会越来越低。另一种更高效的方法是使用FBO,将所有需要处理的OBJ渲染到FBO中,对FBO中得到的纹理进行一次特效处理,然后再输出。这就是“Render to Texture”的典型应用。

Cocos2d - x中的RenderTexture

Cocos2d - x中提供的RenderTexture是GL FBO的封装,但这种封装有些“过度”。它直接将FBO的纹理生成了一个精灵(Sprite),而在一般情况下,我们只需要对FBO中的纹理进行封装即可。如果想要直接控制FBO缓冲区的纹理,就不得不重写一个“Render to Texture”的封装。引擎这样做可能是为了让不熟悉GL的开发者也能像使用Sprite一样方便地使用RenderTexture。但从开发者的角度来看,引擎应该只提供基本功能,尽可能多地将接口留给开发者控制,过度的封装会降低灵活性。

Android平台的问题

使用过Cocos2d - x的RenderTexture的开发者,尤其是Android平台的开发者,可能会遇到一些问题。引擎的源代码中有这样的注释:

// We have not found a way to dispatch the enter background message before the texture data are destroyed.
// So we disable this pair of message handler at present.

大致意思是,在Android平台上,目前还没有找到一种有效的方法来解决游戏切换到后台运行时导致FBO的纹理丢失的问题。这句话是在3.2 - 3.3版本中添加的,在3.0版本的引擎中并没有。在3.0版本的引擎中使用RenderTexture时,确实会发现Android平台存在这个问题,游戏从后台切换回来时,RenderTexture显示为黑色,即FBO丢失,需要像重新加载Texture2D一样重新加载。然而,在3.0版本的RenderTexture构造函数中,明明注册了EVENT_COME_TO_BACKGROUNDEVENT_COME_TO_FOREGROUND事件,但却没有起作用。后来发现,是没有接收到EVENT_COME_TO_BACKGROUND事件,只能接收到EVENT_COME_TO_FOREGROUND事件。

解决方案

修改事件注册方式

首先,修改构造函数中事件注册的方式。将原来的代码:

#if CC_ENABLE_CACHE_TEXTURE_DATA
// Listen this event to save render texture before come to background.
// Then it can be restored after coming to foreground on Android.
auto toBackgroundListener = EventListenerCustom::create(EVENT_COME_TO_BACKGROUND, CC_CALLBACK_1(RenderTexture::listenToBackground, this));
_eventDispatcher->addEventListenerWithSceneGraphPriority(toBackgroundListener, this);
auto toForegroundListener = EventListenerCustom::create(EVENT_COME_TO_FOREGROUND, CC_CALLBACK_1(RenderTexture::listenToForeground, this));
_eventDispatcher->addEventListenerWithSceneGraphPriority(toForegroundListener, this);
#endif

修改为:

#if CC_ENABLE_CACHE_TEXTURE_DATA
// Listen this event to save render texture before come to background.
// Then it can be restored after coming to foreground on Android.
// auto toBackgroundListener = EventListenerCustom::create(EVENT_COME_TO_BACKGROUND, CC_CALLBACK_1(RenderTexture::listenToBackground, this));
// _eventDispatcher->addEventListenerWithSceneGraphPriority(toBackgroundListener, this);
// _eventDispatcher->addEventListenerWithFixedPriority(toBackgroundListener, 1);
auto toForegroundListener = EventListenerCustom::create(EVENT_COME_TO_FOREGROUND, CC_CALLBACK_1(RenderTexture::listenToForeground, this));
// _eventDispatcher->addEventListenerWithSceneGraphPriority(toForegroundListener, this);
_eventDispatcher->addEventListenerWithFixedPriority(toForegroundListener, 1);
#endif

addEventListenerWithSceneGraphPriorityaddEventListenerWithFixedPriority的区别在于,后者允许“事件穿透”,即使节点不处于当前运行场景中,也能接收到事件通知。如果引擎不允许“事件穿透”,这可以被认为是设计上的一个缺陷。

修改listenToForeground函数的实现

其次,修改listenToForeground函数的实现,重新建立FBO和纹理:

void RenderTexture::listenToForeground(EventCustom *event)
{
#if CC_ENABLE_CACHE_TEXTURE_DATA
// -- regenerate frame buffer object and attach the texture
auto texture = new (std::nothrow) Texture2D();
Texture2D* textureCopy;
void *data = nullptr;
do {
// textures must be power of two squared
int powW = 0;
int powH = 0;
if (Configuration::getInstance()->supportsNPOT()) {
powW = _fullviewPort.size.width;
powH = _fullviewPort.size.height;
} else {
powW = ccNextPOT(_fullviewPort.size.width);
powH = ccNextPOT(_fullviewPort.size.height);
}
auto dataLen = powW * powH * 4;
data = malloc(dataLen);
CC_BREAK_IF(! data);
memset(data, 0, dataLen);
if (texture) {
texture->initWithData(data, dataLen, (Texture2D::PixelFormat)_pixelFormat, powW, powH, _fullviewPort.size);
} else {
break;
}
GLint oldRBO;
glGetIntegerv(GL_RENDERBUFFER_BINDING, &oldRBO);
if (Configuration::getInstance()->checkForGLExtension("GL_QCOM")) {
textureCopy = new (std::nothrow) Texture2D();
if (textureCopy) {
textureCopy->initWithData(data, dataLen, (Texture2D::PixelFormat)_pixelFormat, powW, powH, _fullviewPort.size);
} else {
texture->release();
break;
}
}
_texture = texture;
_texture->setAliasTexParameters();
if (_textureCopy) {
_textureCopy = textureCopy;
_textureCopy->setAliasTexParameters();
}
/* It is already deallocated by android */
// glDeleteFramebuffers(1, &_FBO);
// _FBO = 0;
glGenFramebuffers(1, &_FBO);
glBindFramebuffer(GL_FRAMEBUFFER, _FBO);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture->getName(), 0);
CCASSERT(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, "Could not attach texture to framebuffer");
_sprite->setTexture(_texture);
_sprite->setFlippedY(true);
_texture->release();
_sprite->setBlendFunc(BlendFunc::ALPHA_PREMULTIPLIED);
glBindRenderbuffer(GL_RENDERBUFFER, oldRBO);
glBindFramebuffer(GL_FRAMEBUFFER, _oldFBO);
} while (0);
CC_SAFE_FREE(data);
#endif
}

通过以上方法,我们可以解决在Cocos2d - x中使用RenderTexture时,Android平台上FBO纹理丢失的问题。

作者信息

boke

boke

共发布了 3994 篇文章