Cocos2d-x でレイヤーに対してリアルタイムにブラーをかける

ゲームでメニューを出す際に、背景がぼやけて少し暗くなった後に、前面にダイアログが表示される演出があります。そのような画面をぼやかす事をブラーを言います。

今回はこの「ブラー」をCocos2d-xのレイヤーに対してかけてみたいと思います。

成果物

作成したブラーの動作イメージは以下の通りです。ちゃんとリアルタイムにブラーがかかっていることが確認できると思います。

作成環境

以下環境で作成しています。

  • Cocos2d-x 3.16 - Win32プロジェクト
  • Windows10
  • VisualStudio2017

解説

ほぼ、StackOverFlowにあったQAを自分の解釈で書き直しているため、もっと詳細が知りたい方はリンク先を確認してください。*1

ぼやけ方の方法の説明ですが、以下の4枚のレイヤー構成になっています。

まずベースの灰色の部分は全部を残りの3枚のレイヤーの親となります。次に赤いブラーをかけたい自分で作成したレイヤーを上に載せています。ぼかし効果が開始するとその上の二枚のレイヤーは下のレイヤーの情報を自分にぼかしを入れて転写します。これを2枚で2回繰り返して一番上のレイヤーが最終的に表示し効果を発生させています。これを毎フレーム繰り返すことでリアルタイムにブラーを付けることができます。

f:id:Takachan:20180327220532p:plain

コード

早速コードの説明に移ります。

シェーダーファイルを用意する

まずシェーダーファイルを用意します。Resources フォルダの中に "shader" フォルダを作成し以下の2つのファイルを配置します。 先述のリンク先のQAから丸ごとコピーしています。

頂点シェーダー:blur.vert

// blur.vert
attribute vec4 a_position;
attribute vec2 a_texCoord;
attribute vec4 a_color;

varying vec4 v_fragmentColor;
varying vec2 v_texCoord;

void main()
{
    gl_Position = CC_MVPMatrix * a_position;
    v_fragmentColor = a_color;
    v_texCoord = a_texCoord;
}

フラグメントシェーダー:blur.frag

// blur.frag
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;

uniform vec2  u_blurOffset;
uniform float u_blurStrength;

#define MAX_BLUR_WIDHT 10

void main()
{
    vec4 color = texture2D(CC_Texture0, v_texCoord);
    
    float blurWidth = u_blurStrength * float(MAX_BLUR_WIDHT);
    vec4 blurColor  = vec4(color.rgb, 1.0);
    for (int i = 1; i <= MAX_BLUR_WIDHT; ++ i)
    {
        if ( float(i) >= blurWidth )
          break;
        
        float weight = 1.0 - float(i) / blurWidth;
        weight = weight * weight * (3.0 - 2.0 * weight); // smoothstep
    
        vec4 sampleColor1 = texture2D(CC_Texture0, v_texCoord + u_blurOffset * float(i));
        vec4 sampleColor2 = texture2D(CC_Texture0, v_texCoord - u_blurOffset * float(i));
        blurColor += vec4(sampleColor1.rgb + sampleColor2.rgb, 2.0) * weight; 
    }

    gl_FragColor = vec4(blurColor.rgb / blurColor.w, color.a);
}

ぼかしレイヤー

ぼかし効果を付けるために PostProcess(レイヤー)を作成します。

CreateFuncについてはこちらを参照してください。

// PostProcess.hpp
#pragma once
#pragma execution_character_set("utf-8")

#include <vector>
#include <string>
#include "cocos2d.h"

#include "CreateFunc.hpp"

/**
 * ブラー効果を載せるためのレイヤーを表します。
 */
class PostProcess : public cocos2d::Layer, CreateFunc<PostProcess>
{
    cocos2d::GLProgram *_program;
    cocos2d::GLProgramState *_progState;
    cocos2d::RenderTexture *_renderTexture;
    cocos2d::Sprite *_sprite;

public:

    using CreateFunc::create;
    bool init(const std::string& vertexShaderFile, const std::string& fragmentShaderFile);

    void draw(cocos2d::Layer* layer);

    cocos2d::GLProgram* getProgram(void) { return this->_program; }
    cocos2d::GLProgramState* getProgramState(void) { return this->_progState; }
    cocos2d::RenderTexture* getRenderTexture(void) { return this->_renderTexture; }
    cocos2d::Sprite* getSprite(void) { return this->_sprite; }
};

次に実装側です。コンストラクタ内でシェーダーのロードと変数のバインドを行っています。また、draw関数でRenderTextureに引数のレイヤーを写し取る処理が書いてあります。

// PostProcess.cpp
#include <vector>
#include "cocos2d.h"
#include "PostProcess.hpp"

using namespace std;
using namespace cocos2d;

bool PostProcess::init(const std::string& vertexShaderFile, const std::string& fragmentShaderFile)
{
    if (!Layer::init())
    {
        return false;
    }

    this->_program = GLProgram::createWithFilenames(vertexShaderFile, fragmentShaderFile);
    this->_program->bindAttribLocation(
        GLProgram::ATTRIBUTE_NAME_COLOR, GLProgram::VERTEX_ATTRIB_POSITION);
    this->_program->bindAttribLocation(
        GLProgram::ATTRIBUTE_NAME_POSITION, GLProgram::VERTEX_ATTRIB_COLOR);
    this->_program->bindAttribLocation(
        GLProgram::ATTRIBUTE_NAME_TEX_COORD, GLProgram::VERTEX_ATTRIB_TEX_COORD);
    this->_program->bindAttribLocation(
        GLProgram::ATTRIBUTE_NAME_TEX_COORD1, GLProgram::VERTEX_ATTRIB_TEX_COORD1);
    this->_program->bindAttribLocation(
        GLProgram::ATTRIBUTE_NAME_TEX_COORD2, GLProgram::VERTEX_ATTRIB_TEX_COORD2);
    this->_program->bindAttribLocation(
        GLProgram::ATTRIBUTE_NAME_TEX_COORD3, GLProgram::VERTEX_ATTRIB_TEX_COORD3);
    this->_program->bindAttribLocation(
        GLProgram::ATTRIBUTE_NAME_NORMAL, GLProgram::VERTEX_ATTRIB_NORMAL);
    this->_program->bindAttribLocation(
        GLProgram::ATTRIBUTE_NAME_BLEND_WEIGHT, GLProgram::VERTEX_ATTRIB_BLEND_WEIGHT);
    this->_program->bindAttribLocation(
        GLProgram::ATTRIBUTE_NAME_BLEND_INDEX, GLProgram::VERTEX_ATTRIB_BLEND_INDEX);
    this->_program->link();

    this->_progState = GLProgramState::getOrCreateWithGLProgram(this->_program);
    this->_program->updateUniforms();

    Size visibleSize = Director::getInstance()->getVisibleSize();
    this->_renderTexture = RenderTexture::create(visibleSize.width, visibleSize.height);
    this->_renderTexture->retain();

    this->_sprite = Sprite::createWithTexture(this->_renderTexture->getSprite()->getTexture());
    this->_sprite->setTextureRect(Rect(0, 0, this->_sprite->getTexture()->getContentSize().width,
    this->_sprite->getTexture()->getContentSize().height));
    this->_sprite->setAnchorPoint(Point::ZERO);
    this->_sprite->setPosition(Point::ZERO);
    this->_sprite->setFlippedY(true);
    this->_sprite->setGLProgram(this->_program);
    this->_sprite->setGLProgramState(this->_progState);

    this->addChild(_sprite);

    return true;
}

void PostProcess::draw(Layer* layer)
{
    this->_renderTexture->beginWithClear(0, 0, 0, 0);
    layer->visit();
    this->_renderTexture->end();
}

ブラーレイヤー

次に上記のクラスを制御するために Blur (レイヤー)という名前でまとめて制御するクラスを作成します。

// Blur.hpp
#pragma once
#pragma execution_character_set("utf-8")

#include <chrono>
#include <vector>
#include "cocos2d.h"
#include "CreateFunc.hpp"

class PostProcess;

/**
 * Blur効果を発生させるためのレイヤーを表します。
 * 
 * 参考
 * https://stackoverflow.com/questions/46745199/fast-gaussian-blur-at-pause
 * https://github.com/Rabbid76/cpp-examples/tree/master/cocos2d/cpp-shader-blur
 */
class Blur : public cocos2d::Layer, CreateFunc<Blur>
{
    cocos2d::Layer* _gameLayer;
    PostProcess* _blurPass1_PostProcessLayer;
    PostProcess* _blurPass2_PostProcessLayer;
    
    // ブラー効果を開始したかどうかを表すフラグ
    bool _blurBegin = false;

    // beginBlurメソッドの引数を覚えておく用の変数
    float _targetStrength = 0.0f;
    float _duration = 0.0f;
    // beginした時刻の記録用の変数
    std::chrono::high_resolution_clock::time_point _startTime;

public:
    bool init(cocos2d::Layer* gameLayer);
    using CreateFunc::create;

    // ブラーアニメーションを開始する
    //
    // blurStrength : 1より大きいとブラーが強くかかる。1よりちいさいと弱くかかる。
    // durationFrame : アニメーションが終了するまでのフレーム数
    //                 だいたい100以上を指定しても意味ない
    //
    // 短い時間に強くかけるとか、長い時間で弱くかけることで量の調整ができる。
    // 但しデフォルトが一番きれいに表示される。
    //
    void beginBlur(float _duration = 0.5f, float _targetStrength = 1.0f);

    // ブラー効果の発生を終了する
    void clearBlur();

    //// ブラー効果が終了したかどうかを表すフラグを取得する
    //bool isBlurCompleted() { return this->_blurCompleted; }

    // 1フレームごとの処理
    virtual void update(float delta) override;
};

次に実装です。beginBlur 関数でブラーをかけ始めるのですが、この関数では開始トリガーだけかけて、残りはすべて update 関数で処理を行います。かなり長めです。

// Blur.cpp
#include <math.h>
#include "PostProcess.hpp"
#include "Blur.hpp"

using namespace std;
using namespace std::chrono;
using namespace cocos2d;

bool Blur::init(cocos2d::Layer * gameLayer)
{
    if (!Layer::init())
    {
        return false;
    }

    this->_gameLayer = gameLayer;
    this->addChild(this->_gameLayer, 0);

    // blur layer even
    this->_blurPass1_PostProcessLayer = PostProcess::create("shader/blur.vert", "shader/blur.frag");
    this->_blurPass1_PostProcessLayer->setVisible(false);
    this->_blurPass1_PostProcessLayer->setAnchorPoint(Point::ZERO);
    this->_blurPass1_PostProcessLayer->setPosition(Point::ZERO);
    this->addChild(this->_blurPass1_PostProcessLayer, 1);

    // blur layer odd
    this->_blurPass2_PostProcessLayer = PostProcess::create("shader/blur.vert", "shader/blur.frag");
    this->_blurPass2_PostProcessLayer->setVisible(false);
    this->_blurPass2_PostProcessLayer->setAnchorPoint(Point::ZERO);
    this->_blurPass2_PostProcessLayer->setPosition(Point::ZERO);
    this->addChild(this->_blurPass2_PostProcessLayer, 1);

    this->clearBlur();

    this->scheduleUpdate();

    return true;
}

void Blur::beginBlur(float _duration, float _targetStrength)
{
    this->_startTime = high_resolution_clock::now();
    this->_targetStrength = _targetStrength;
    this->_duration = _duration;

    this->_blurBegin = true;
}

void Blur::clearBlur()
{
    this->_blurBegin = false;

    this->_gameLayer->setVisible(true);
    this->_blurPass1_PostProcessLayer->setVisible(false);
    this->_blurPass2_PostProcessLayer->setVisible(false);
}

void Blur::update(float delta)
{
    if (!this->_blurBegin)
    {
        return;
    }

    this->_gameLayer->setVisible(true);
    this->_blurPass1_PostProcessLayer->setVisible(false);
    this->_blurPass2_PostProcessLayer->setVisible(true);

    // ブラーを時間経過で強める
    high_resolution_clock::time_point currentTime = high_resolution_clock::now();
    double elapsedms = duration_cast<milliseconds>(currentTime - this->_startTime).count();
    double blurStrength = this->_targetStrength * (elapsedms / 1000.0) / this->_duration;

    if (blurStrength > this->_targetStrength)
    {
        blurStrength = this->_targetStrength; // もし途中でも打ち切る
    }

    Size visibleSize = Director::getInstance()->getVisibleSize();

    // ぼかし効果(1)
    GLProgramState* blurPass1state = this->_blurPass1_PostProcessLayer->getProgramState();
    blurPass1state->setUniformVec2("u_blurOffset", Vec2(1.0f / visibleSize.width, 0.0));
    blurPass1state->setUniformFloat("u_blurStrength", (float)blurStrength);

    this->_gameLayer->setVisible(true);
    this->_blurPass1_PostProcessLayer->draw(this->_gameLayer);
    this->_gameLayer->setVisible(false);

    // ぼかし効果(2)
    GLProgramState* blurPass2state = this->_blurPass2_PostProcessLayer->getProgramState();
    blurPass2state->setUniformVec2("u_blurOffset", Vec2(0.0, 1.0f / visibleSize.height));
    blurPass2state->setUniformFloat("u_blurStrength", (float)blurStrength);

    this->_blurPass1_PostProcessLayer->setVisible(true);
    this->_blurPass2_PostProcessLayer->draw(this->_blurPass1_PostProcessLayer);
    this->_blurPass1_PostProcessLayer->setVisible(false);
}

利用する側のコード

以上で、ブラー効果の実装は終了です。次は実際にブラーを利用する側のコードです。

冒頭の動画の背景は、Cocos2d-x のテンプレートの HelloWorld に実装ているのでそこの部分を抜粋します。

// HelloWorld.hpp
#pragma once

#include "cocos2d.h"
#include "CreateFunc.hpp"

class Bullet;
class StaticAnimationBlurLayer;
class Blur;

class HelloWorld : public cocos2d::Layer, CreateFunc<HelloWorld>
{
public:
    // デバッグ用制御のため変数を公開
    Blur* _blurLayer; 

    virtual bool init();
    using CreateFunc::create;
};

実装側コード:

// HelloWorld.cpp
//#pragma execution_character_set("utf-8")

#include <stdlib.h>
#include <vector>
#include <cmath>
#include "HelloWorldScene.hpp"
#include "Blur.hpp"

using namespace cocos2d;
using namespace cocos2d::ui;

Scene* HelloWorld::createScene()
{
    // 'scene' is an autorelease object
    auto scene = Scene::create();

    // 'layer' is an autorelease object
    auto layer = HelloWorld::create();
    auto snowLayer = SnowLayer::create();

    //layer->addChild(snowLayer);

    auto tindLayer = TouchIndicateLayer::create();

    // add layer as a child to scene
    
    //auto blurLayer = StaticAnimationBlurLayer::create(layer);
    auto blurLayer = Blur::create(layer);
    layer->_blurLayer = blurLayer;

    //scene->addChild(snowLayer);
    //scene->addChild(layer);
    scene->addChild(blurLayer);
    scene->addChild(tindLayer);

    // return the scene
    return scene;
}

bool HelloWorld::init()
{
    if (!Layer::init())
    {
        return false;
    }

    Size visibleSize = Director::getInstance()->getVisibleSize();

    // 背景画像の設定
    Sprite* sp = Sprite::create("background.png");
    sp->setPosition(Vec2(visibleSize.width / 2, visibleSize.height / 2));
    this->addChild(sp);

    // リアルタイムにブラーがかかってるか確認するためのハチが左右に飛ぶ動作
    Sprite* hachi = Sprite::create("hachi.png");
    hachi->getTexture()->setAntiAliasTexParameters();
    hachi->setPosition(Vec2(visibleSize.width / 2 - 150 / 2, visibleSize.height / 2));
    hachi->setRotation(90);
    this->addChild(hachi);

    auto action =
        Repeat::create(
            Sequence::create(
            EaseInOut::create(MoveBy::create(1.5f, Vec2(150, 0)), 2.2f),
            RotateBy::create(0, 180),
            EaseInOut::create(MoveBy::create(1.5f, Vec2(-150, 0)), 2.2f),
            RotateBy::create(0, 180), nullptr), -1);

    hachi->runAction(action);

    // デバッグ用、画面をタッチしたらブラーをかける
    EventListenerTouchOneByOne* _evListener = EventListenerTouchOneByOne::create();
    _evListener->onTouchBegan = [this](Touch* touch, Event* event)
    {
        this->_blurLayer->beginBlur(); // ここで開始を呼び出している
        return false;
    };
    _eventDispatcher->addEventListenerWithSceneGraphPriority(_evListener, this);

    return true;
}

という風にブラーをかけたいレイヤーを定義しておいて CreateScene 関数に以下のように記述します。

Scene* SceneFactory::createScene()
{
    // ベースとなる Scene
    auto scene = Scene::create();

    // Blurオブジェクトにブラーをかけたいレイヤーを指定してインスタンスを作る
    auto layer = HelloWorld::create();
    auto blurLayer = Blur::create(layer);
    layer->_blurLayer = blurLayer;

    scene->addChild(blurLayer);
    scene->addChild(tindLayer);

    return scene;
}

これで、AppDelegate::applicationDidFinishLaunching() で上記のコードを呼び出せばブラーの設定ができます。

ブラーを強めにかけたいときは、Blur クラスの beginBlur(時間(sec), ブラー強さ) なので第2に引数に1.0より大きな値を入れます。大きくても3.0くらいで十分だと思います。時間を短くかけたいときは第1引数を 0.25f (秒) などにすれば指定した強さのブラーが素早くかかります。

長くなりましたが以上です。

*1:この情報がなかったら制作不能だったので大変感謝しています。