ゲームでメニューを出す際に、背景がぼやけて少し暗くなった後に、前面にダイアログが表示される演出があります。そのような画面をぼやかす事をブラーを言います。
今回はこの「ブラー」をCocos2d-xのレイヤーに対してかけてみたいと思います。
成果物
作成したブラーの動作イメージは以下の通りです。ちゃんとリアルタイムにブラーがかかっていることが確認できると思います。
作成環境
以下環境で作成しています。
- Cocos2d-x 3.16 - Win32プロジェクト
- Windows10
- VisualStudio2017
解説
ほぼ、StackOverFlowにあったQAを自分の解釈で書き直しているため、もっと詳細が知りたい方はリンク先を確認してください。*1
ぼやけ方の方法の説明ですが、以下の4枚のレイヤー構成になっています。
まずベースの灰色の部分は全部を残りの3枚のレイヤーの親となります。次に赤いブラーをかけたい自分で作成したレイヤーを上に載せています。ぼかし効果が開始するとその上の二枚のレイヤーは下のレイヤーの情報を自分にぼかしを入れて転写します。これを2枚で2回繰り返して一番上のレイヤーが最終的に表示し効果を発生させています。これを毎フレーム繰り返すことでリアルタイムにブラーを付けることができます。
コード
早速コードの説明に移ります。
シェーダーファイルを用意する
まずシェーダーファイルを用意します。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:この情報がなかったら制作不能だったので大変感謝しています。