(cocos2d-x) 背景模糊化以及效率优化

本文探讨了在cocos2d-x游戏中背景模糊化处理的三种方案,包括直接对精灵模糊、渲染到纹理模糊以及优化后的精灵模糊化。方案0和1在效率和效果上存在问题,而方案2通过截屏并生成普通精灵的方式,成功解决了效率问题,实现在移动设备上保持60帧的流畅体验。文章提供了相关代码实现。

在游戏中有时了为了突显前景元素,需要对背景进行一些模糊化处理,如下图所示

虽然cocos2d-x 3.x提供了新的用户模糊化的shader脚本,但根据实现方式的不同仍然会有很大区别。


方案0:对所有背景上所有精灵进行模糊化

最不可取的方案,多数情况下并不能达到预期效果,而且效率低,原因见以下两图:

第1张黑色背景是引擎示例中的效果,第2张是我在示例代码中加了一个白色背景后的效果。

这里的问题是,实际上3.x版本提供shader脚本无法很好的处理带有透明度的图片,由于示例默认都是黑色背景,因此无法看出问题,而在白色背景下,所有透明的地方都被处理成了黑色。相信现在游戏中即使背景也会有大量带有透明区域的图片,况且当我们我们模糊处理的时候经常也需要处理前景图片,那么这个问题完全不能忽视了。

我本人对shader脚本并不太熟悉,不过猜测应该可以靠修改shader脚本解决。但即使如此,对场景中多个精灵进行模糊化,效率如何呢?我们来看方案1。


方案1:截取屏幕生成一个精灵作为临时背景,并对此精灵进行模糊化

基本思路是使用渲染到纹理技术生成一个临时的图片精灵,将此精灵置于场景最上层(即Zorder高于其他精灵),成为一个假背景,而把需要突显的元素,再置于这个精灵之上。实现很容易,效果如图:

粗略看貌似没问题,但请注意左下角的帧率。实际上即使只对一个960x640尺寸的精灵进行模糊化,效率也非常低,长期保持在20帧以下。虽然这里我的运行环境是集成显卡,这也是造成帧率的低的原因之一,但台式机尚且如此,移动设备上很难表现的更好,况且移动设备还要耗电和发热问题,效率问题必须重视。于是便有了方案2。


方案2:用模糊化后的精灵生成一个普通精灵作为临时背景

效果如图:

帧数恢复到了60左右,经我在手机上测试,对1280x720的图片进行这样的处理,也几乎没有问题,仅仅会在生成模糊图的一刻会瞬间降到40帧左右,之后会迅速恢复至60帧。

以下是实现代码:

#include "HelloWorldScene.h"

USING_NS_CC;

//////////////////////////////////////////////////////////////////////////
//精灵模糊类
//////////////////////////////////////////////////////////////////////////
class SpriteBlur : public cocos2d::Sprite
{
public:
    SpriteBlur();
    ~SpriteBlur();
    static SpriteBlur* create(cocos2d::Sprite* pSprite, const float fRadius = 8.0f, const float fSampleNum = 8.0f);
    bool initWithTexture(cocos2d::Texture2D* pTexture, const cocos2d::Rect&  rRect);
    void initGLProgram();

    void setBlurRadius(float fRadius);
    void setBlurSampleNum(float fSampleNum);

protected:
    float fBlurRadius_;
    float fBlurSampleNum_;
};

SpriteBlur::SpriteBlur()
    : fBlurRadius_(0.0f)
    , fBlurSampleNum_(0.0f)
{

}

SpriteBlur::~SpriteBlur()
{

}

SpriteBlur* SpriteBlur::create(cocos2d::Sprite* pSprite, const float fRadius, const float fSampleNum)
{
    SpriteBlur* pRet = new (std::nothrow) SpriteBlur();
    if (nullptr == pRet)
        return nullptr;
    pRet->fBlurRadius_ = fRadius;
    pRet->fBlurSampleNum_ = fSampleNum;
    if (pRet->initWithSpriteFrame(pSprite->getSpriteFrame()))
        pRet->autorelease();
    else
        CC_SAFE_DELETE(pRet);
    return pRet;
}

bool SpriteBlur::initWithTexture(cocos2d::Texture2D* texture, const cocos2d::Rect& rect)
{
    if (Sprite::initWithTexture(texture, rect))
    {
#if CC_ENABLE_CACHE_TEXTURE_DATA
        auto listener = EventListenerCustom::create(EVENT_RENDERER_RECREATED, [this](EventCustom* event){
            setGLProgram(nullptr);
            initGLProgram();
        });

        _eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
#endif
        initGLProgram();
        return true;
    }
    return false;
}

void SpriteBlur::initGLProgram()
{
    GLchar * fragSource = (GLchar*)cocos2d::String::createWithContentsOfFile(
        cocos2d::FileUtils::getInstance()->fullPathForFilename("shaders/blur.fsh").c_str())->getCString();
    auto program = cocos2d::GLProgram::createWithByteArrays(ccPositionTextureColor_noMVP_vert, fragSource);

    auto glProgramState = cocos2d::GLProgramState::getOrCreateWithGLProgram(program);
    setGLProgramState(glProgramState);

    auto size = getTexture()->getContentSizeInPixels();
    getGLProgramState()->setUniformVec2("resolution", size);
    getGLProgramState()->setUniformFloat("blurRadius", fBlurRadius_);
    getGLProgramState()->setUniformFloat("sampleNum", fBlurSampleNum_);
}


void SpriteBlur::setBlurRadius(float radius)
{
    fBlurRadius_ = radius;
    getGLProgramState()->setUniformFloat("blurRadius", fBlurRadius_);
}

void SpriteBlur::setBlurSampleNum(float num)
{
    fBlurSampleNum_ = num;
    getGLProgramState()->setUniformFloat("sampleNum", fBlurSampleNum_);
}

//////////////////////////////////////////////////////////////////////////
//精灵模糊化函数
//////////////////////////////////////////////////////////////////////////
cocos2d::RenderTexture* SpriteBlurer(cocos2d::Sprite* pSprite, const float fRadius = 8.0f, const float fSampleNum = 8.0f)
{
    //模糊化的临时精灵
    auto pSptBlur = SpriteBlur::create(pSprite, fRadius, fSampleNum);
    pSptBlur->setRotationSkewX(180.0f);
    pSptBlur->setPositionX(pSptBlur->getContentSize().width / 2);
    pSptBlur->setPositionY(pSptBlur->getContentSize().height / 2);
    //使用精灵尺寸初始化一个空的渲染纹理对象
    cocos2d::RenderTexture* textureScreen =
        cocos2d::RenderTexture::create(pSptBlur->getContentSize().width, pSptBlur->getContentSize().height);
    //开始获取
    textureScreen->beginWithClear(0.0f, 0.0f, 0.0f, 0.0f);
    //遍历节点对象,填充纹理到texure中
    pSptBlur->visit();
    //结束获取
    textureScreen->end();
    return textureScreen;
}

//////////////////////////////////////////////////////////////////////////
//截屏函数
//////////////////////////////////////////////////////////////////////////
cocos2d::RenderTexture* ScreenShot(const bool bIsSave, std::function<void(cocos2d::RenderTexture*, const std::string&)> pFuncCallback)

{
    //使用屏幕尺寸初始化一个空的渲染纹理对象
    Size sizeWin = Director::getInstance()->getWinSize();
    cocos2d::RenderTexture* textureScreen =
        cocos2d::RenderTexture::create(sizeWin.width, sizeWin.height);
    //清除数据并开始获取
    textureScreen->beginWithClear(0.0f, 0.0f, 0.0f, 0.0f);
    //遍历场景节点对象,填充纹理到texure中
    cocos2d::Director::getInstance()->getRunningScene()->visit();
    //结束获取
    textureScreen->end();
    //保存为PNG图
    if (bIsSave)
    {
        static int s_iSerialNumber = 0;
        textureScreen->saveToFile(
            cocos2d::CCString::createWithFormat("ScreenShot_%04d.png", ++s_iSerialNumber)->getCString(),
            cocos2d::Image::Format::PNG,
            true,
            pFuncCallback);
    }
    else
    {
        if (nullptr != pFuncCallback)
        {
            pFuncCallback(textureScreen, "");
        }
    }
    return textureScreen;
}


//////////////////////////////////////////////////////////////////////////
//示例
//////////////////////////////////////////////////////////////////////////
Scene* HelloWorld::createScene()
{
    auto scene = Scene::create();
    auto layer = HelloWorld::create();
    scene->addChild(layer);
    return scene;
}

bool HelloWorld::init()
{
    if ( !Layer::init() )
    {
        return false;
    }
    Size visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();
    auto closeItem = MenuItemImage::create(
                                           "CloseNormal.png",
                                           "CloseSelected.png",
                                           CC_CALLBACK_1(HelloWorld::menuCloseCallback, this));
    closeItem->setPosition(Vec2(origin.x + visibleSize.width - closeItem->getContentSize().width/2 ,
                                origin.y + closeItem->getContentSize().height/2));
    auto menu = Menu::create(closeItem, NULL);
    menu->setPosition(Vec2::ZERO);
    this->addChild(menu, 1);
    auto label = Label::createWithTTF("Hello World", "fonts/Marker Felt.ttf", 24);
    label->setPosition(Vec2(origin.x + visibleSize.width/2,
                            origin.y + visibleSize.height - label->getContentSize().height));
    this->addChild(label, 1);
    auto sprite = Sprite::create("HelloWorld.png");
    sprite->setPosition(Vec2(visibleSize.width/2 + origin.x, visibleSize.height/2 + origin.y));
    this->addChild(sprite, 0);

    //////////////////////////////////////////////////////////////////////////
    //背景模糊
    //////////////////////////////////////////////////////////////////////////
    //添加按键用于触发背景模糊
    auto blurItem = MenuItemImage::create(
        "CloseNormal.png",
        "CloseSelected.png",
        [&](Ref* pSender){

            //截取屏幕(不保存图片,不设置回调函数)
            auto textureScreen = ScreenShot(false, nullptr);
            
            //方案1:直接对背景精灵进行模糊化操作(效率低)
            //使用截取后获得的截图精灵,生成一个带模糊效果的精灵
            //SpriteBlur* spriteBlur = SpriteBlur::create(textureScreen->getSprite(), 8.0f, 8.0f);
            //spriteBlur->setRotationSkewX(180.0f);

            //方案2:将模糊化后的精灵保存成一张图片
            //将截取的屏幕进行模糊化
            auto textureBlur = SpriteBlurer(textureScreen->getSprite());
            //将模糊化后的图片保存成一张图片
            auto spriteBlur = Sprite::createWithSpriteFrame(textureBlur->getSprite()->getSpriteFrame());
            spriteBlur->setPosition(Vec2(visibleSize.width, visibleSize.height));

            Size size = Director::getInstance()->getWinSize();
            spriteBlur->setRotationSkewX(180.0f);
            spriteBlur->setPosition(Vec2(size.width / 2, size.height / 2));

            //将此精灵覆盖在原有层之上,成为一个临时背景
            this->addChild(spriteBlur, 4);

            //添加一个新的图片作为前景元素以便进行比较
            auto sprite = Sprite::create("HelloWorld.png");
            sprite->setPosition(Vec2(size.width / 2, size.height / 2));
            sprite->setScale(0.5f);
            this->addChild(sprite, 5);
            
        }
    );
    blurItem->setPosition(Vec2(visibleSize.width / 2, visibleSize.height / 2));
    menu->addChild(blurItem);

    return true;
}


void HelloWorld::menuCloseCallback(Ref* pSender)
{
#if (CC_TARGET_PLATFORM == CC_PLATFORM_WP8) || (CC_TARGET_PLATFORM == CC_PLATFORM_WINRT)
    MessageBox("You pressed the close button. Windows Store Apps do not implement a close button.","Alert");
    return;
#endif

    Director::getInstance()->end();

#if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS)
    exit(0);
#endif
}

HelloWorldScene.h未做任何修改,因此此处不再贴出。

class SpriteBlur用于生成可被shader处理的精灵,注意将cocos2d-x的shader脚本拷入自己的项目中。得到的精灵每一帧都会进行像素渲染,可以动态调节模糊效果。构造函数中的除了需要模糊化的精灵之外,另外两个参数:fRadius为模糊半径,此参数越大,模糊效果越明显;fSampleNum为模糊采样,此参数越大,模糊效果越细腻。这两个参数越高,效率也就越低。并且当高过一定数值后,在手机上有可能出现异常,建议两个参数设置均不要超过8。

ScreenShot为截屏函数,相关教程很多,此处不再展开。

SpriteBlurer为精灵模糊化函数,将一个普通精灵传入,即可返回一个被模糊化处理后的纹理,使用此纹理即可生成普通精灵。即把用shader处理过的精灵截图,生成普通精灵,此精灵不再需要进行像素渲染,因此保证了效率。


此方案的缺点:SpriteBlurer函数生成的纹理,既不能再次调整模糊程度的强弱,也不能改变显示内容。但如果作为一个背景来用,这个方案已经足够了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值