Cocos2d-xでルール画像を使ってシーントランシジョンする(シェーダー使用)

Unityだと以下のように紹介されている、ルール画像を用いたシーンの遷移エフェクトをCocos2d-xで実装してみたいと思います。

tsubakit1.hateblo.jp

まずこんな感じのグレースケールのマスク画像を使用します。

f:id:Takachan:20190913234636p:plain

画面遷移はこんな感じになります。

f:id:Takachan:20190913234701g:plain

「マスク画像」を配布されているFor youさんの画像を使用させていただきました。

実装環境

今回の実装&確認環境は以下の通りです。

  • Cocos2d-x 3.17.1
  • VisualStudio2017 15.9.14
  • Windows10

.oO(すいませんモバイルで動作確認してません。多分動くの精神で…)

作ったトランシジョンの使い方

というわけでまずは作ったものの使い方です。

他のTransitionと同じくTransitionクラスを作成してDirectorに指示します。

#include "TransitionPattern.hpp"

void foo()
{
    // 次のシーン
    auto nextScene = Scene::create();

    // 一般的な演出効果付きのシーン遷移っぽく指定する
    // 引数:
    //   [1] : 演出効果の時間
    //   [2] : 次に遷移するシーン
    //   [3] : 使用するマスク画像のパス
    //   [4] : アルファマスクを使用する場合はtrue, 使用しない場合はfalse
    auto transition = TransitionPattern::create(2.0f, next, "ptn.png", true);
    Director::getInstance()->replaceScene(transition);

    // 以下お好みで,,,
    // アルファマスクを使用するときのアルファ強度を0.0f ~ 1.0f で指定する
    // → 既定で0.5fが設定されています。
    // 大い値を指定するとより急な変化になります。1.0f で [4] = falseと同じ状態になります。
    transition->getTransitionLayer()->setStlength(0.75f);
}

たったこれだけです。簡単ですね。

実装は相当長いですがよろしければお付き合いください。

シェーダーの準備

まずマスク画像の濃淡を考慮して背景を変化量で透過したりしなかったりするシェーダを準備します。

頂点シェーダー

C++のコードとして直接記述するので以下のように特殊文字をエスケープしてファイルに保存します。

// pattern_trans.vert

const GLchar* PatternTransitionVert = 
STRINGIFY(

attribute vec4 a_position;
attribute vec2 a_texCoord;
attribute vec4 a_color;

\n#ifdef GL_ES\n
varying lowp vec4 v_fragmentColor;
varying mediump vec2 v_texCoord;
\n#else\n
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
\n#endif\n

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

いわゆる、何もしない頂点シェーダーです。

フラグメントシェーダー

フラグメントシェーダーはアルファマスクする版としない版の2種類を以下のように準備します。

アルファマスクしない版

// pattern_trans.frag

const GLchar* PatternTransitionFrag = STRINGIFY(

\n#ifdef GL_ES\n
precision mediump float;
\n#endif\n

varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
uniform float u_percent;

void main(void)
{
    vec4 color = v_fragmentColor * texture2D(CC_Texture0, v_texCoord);
    if(color.x > u_percent)
    {
        gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
    }
}
);

アルファマスクする版

// pattern_trans_alpha.frag

const GLchar* PatternTransitionFragAlpha = STRINGIFY(

\n#ifdef GL_ES\n
precision mediump float;
\n#endif\n

varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
uniform float u_percent;
uniform float u_stlength;

void main(void)
{
    vec4 color = v_fragmentColor * texture2D(CC_Texture0, v_texCoord);
    
    float a = 1.0 / u_stlength;
    gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0 - (((u_percent / color.x) - u_stlength) * a));
}
);

Cocos2d-xから受け取る"u_percent"が0.0~1.0まで変化するときにマスク画像の白黒の考慮して背景を透過するかしないかを決定するシェーダーになります。

アルファマスクがある方は、変化時刻が近くなってくると徐々に変化が開始されるように変化の遅延と目標時刻に到達したときに完全にマスクがされるように調整しています。

シェーダーをC++コードから利用する

上記シェーダー文字列をC++側から使用するため以下のコードを記述します。

// Shaders.hpp

#pragma once
#pragma execution_character_set("utf-8")

#include "cocos2d.h"

NS_CC_BEGIN

extern const GLchar* PatternTransitionVert;
extern const GLchar* PatternTransitionFrag;
extern const GLchar* PatternTransitionFragAlpha;

NS_CC_END

で、実体の展開は#includeでやります。

includeを只の文字列展開マクロ扱いは久しぶりです。存在を忘れきっていました。

// Shaders.cpp

#include "Shaders.hpp"

#define STRINGIFY(A) #A

NS_CC_BEGIN

#include "pattern_trans.frag"
#include "pattern_trans.vert"
#include "pattern_trans_alpha.frag"

NS_CC_END

PatternTransitionLayerクラスの作成

最終的にTransitionとして利用しますが、そこで使用するクラスをPatternTransitionLayerとして事前に作成しておきます。

先述のシェーダーをシーン上に配置したマスク画像に設定して処理を開始するためのクラスを作成します。

ヘッダー

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

#include "CreateFunc.hpp"

#include "cocos2d.h"

/**
 * ルール画像を用いたトランジションの実装
 */
class PatternTransitionLayer : public cocos2d::Layer, CreateFunc<PatternTransitionLayer>
{
public:
    
    // ルール画像名を指定してオブジェクトを初期化する
    // useAlpha : アルファトランジションするかどうかの指定。true : する / false : しない(既定値)
    bool init(std::string ruleImage, bool useAlpha = false);
    using CreateFunc::create;

    // トランジションの方法
    enum class TransitionType
    {
        // シーンの入り(黒 → 白へ非表示にしていく
        FadeIn,
        // シーンのはけ(白 → 黒に表示していく
        FadeOut,
    };

    // 'beginTransition' 関数が終了したときに呼び出されるメソッド
    void setTransitionCompleted(std::function<void(PatternTransitionLayer* self)> handler)
    {
        this->handler_beginTransitionCompleted = handler;
    }

    // トランシジョンの時間と種類を指定してエフェクトを開始する
    void beginTransition(TransitionType type, float dulation/*sec*/);

    // アルファトランジションを使用するときの変化量を0.0~1.0の範囲で設定する(既定値は0.5)
    void setStlength(float value);

    // フレームごとの制御
    void update(float delta) override;

protected: /* methods */

    // call by 'setStlength' 
    void _setStlength(float value);

protected: /* fields */

    // EventHanlder
    std::function<void(PatternTransitionLayer* self)> handler_beginTransitionCompleted = nullptr;
    // パターン画像用
    cocos2d::Sprite* pattern = nullptr;
    // シェーダー用の変数
    cocos2d::GLProgram* program = nullptr;
    cocos2d::GLProgramState* state = nullptr;
    // 変化の方法
    TransitionType type = TransitionType::FadeOut;
    // 完了までの時間
    float dulation = .0f;
    // 経過時間
    float elapsed = .0f;
    // 実行中かどうか
    bool isUpdating = false;
    // アルファトランシジョンを使用するかどうかのフラグ
    bool useAlpha = false;
};

実装

上記ヘッダーの実装は以下の通りです。

#include "PatternTransitionLayer.hpp"
#include "Shaders.hpp"

#include "CommonFunction.hpp"

using namespace std;
using namespace cocos2d;

bool PatternTransitionLayer::init(std::string ruleImage, bool useAlpha)
{
    if (!Layer::init())
    {
        return false;
    }

    this->pattern = Sprite::create(ruleImage);


    float xscale = __WIDTH(this) / __WIDTH(this->pattern);
    float yscale = __HEIGHT(this) / __HEIGHT(this->pattern);
    this->pattern->setScale(xscale, yscale);
    this->pattern->setPosition(this->getContentSize() / 2.0f);
    this->pattern->setVisible(false);
    this->addChild(pattern);

    this->useAlpha = useAlpha;
    const GLchar* fag = useAlpha ? cocos2d::PatternTransitionFragAlpha : cocos2d::PatternTransitionFrag;
    this->program = GLProgram::createWithByteArrays(cocos2d::PatternTransitionVert, fag);

    this->state = GLProgramState::create(this->program);
    this->pattern->setGLProgramState(this->state);

    this->_setStlength(0.5f);

    return true;
}

void PatternTransitionLayer::beginTransition(TransitionType type, float dulation)
{
    if (this->isUpdating)
    {
        CCLOG("alrealy updating");
        return;
    }

    this->type = type;
    this->elapsed = 0.0;
    this->dulation = dulation;
    this->isUpdating = true;
    this->pattern->setVisible(true);
    this->scheduleUpdate();
}

void PatternTransitionLayer::setStlength(float value)
{
    this->_setStlength(value);
}

void PatternTransitionLayer::update(float delta)
{
    this->elapsed += delta;
    if (this->elapsed > this->dulation)
    {
        // 最後少しだけ残っちゃうので強制的に最終値を設定
        this->state->setUniformFloat("u_percent", this->type == TransitionType::FadeOut ? .0f : 1.0f);

        this->unscheduleUpdate();
        this->isUpdating = false;
        invoke_fn(this->handler_beginTransitionCompleted, this);
        return;
    }

    float percent = this->elapsed / this->dulation;
    if (this->type == TransitionType::FadeOut)
    {
        percent = abs(1.0f - percent); // フェードアウトの時は反対にする
    }

    this->state->setUniformFloat("u_percent", percent);
}

void PatternTransitionLayer::_setStlength(float value)
{
    if (!useAlpha)
    {
        CCLOG("not use alpha.");
        return;
    }

    if (this->isUpdating)
    {
        CCLOG("alrealy updating");
        return;
    }

    if (value > 1 || value < 0)
    {
        CCLOG("value is out of range.");
        return;
    }

    this->state->setUniformFloat("u_stlength", value);
}

"init"でシェーダーを設定して"beginTransition"で演出を開始、"update"で時間の変化をシェーダーに"u_percent"という共有変数で伝えています。

TransitionPatternクラスの作成

上記をCocos2d-xの演出付きのシーン遷移に組み込みます。

ヘッダー

"cocos2d::TransitionScene"クラスを継承して"onEnter"と"onExit"でそれぞれ演出の開始、終了を記述するようです。

あんまり仕組みを理解してないですがまぁたぶんこんな感じで大丈夫だと思います。

// TransitionPattern.hpp

#pragma once
#pragma execution_character_set("utf-8")

#include "CreateFunc.hpp"

#include "cocos2d.h"

class PatternTransitionLayer;

/**
 * パターン画像を指定した画面遷移を実行します。
 */
class TransitionPattern : public cocos2d::TransitionScene, CreateFunc<TransitionPattern>
{
public:

    using CreateFunc::create;
    bool init(float duration, Scene* scene, std::string ruleImage, bool useAlpha = false);

    virtual void onEnter() override;
    virtual void onExit() override;

    // Stlrngthを設定したいときはエフェクトソースを取得する
    PatternTransitionLayer* getTransitionLayer() { return this->effect; }

protected:

    static const unsigned int kSceneFade;

    // フェードアウトしたかどうかのフラグ
    // true : フェードアウト済み / false : まだ
    bool doneFadeOut = false;
    // エフェクトを発生させるためのレイヤー
    PatternTransitionLayer* effect = nullptr;
};

実装

onEnterで演出をすべて記述します。

フェードアウトが終了したら"TransitionScene::hideOutShowIn"を呼び出してCocos2d-xに通知を行うと次のシーンに自動で遷移して、フェードインの演出が始まります。

フェードインが終了したときには"TransitionScene::finish"を呼び出すと"onExit"が呼び出されるのでそこで演出の破棄を行います。

// TransitionPattern.cpp

#include "TransitionPattern.hpp"

#include "PatternTransitionLayer.hpp"

using namespace std;
using namespace cocos2d;

const unsigned int TransitionPattern::kSceneFade = 0xFADEFADE;

bool TransitionPattern::init(float duration, Scene* scene, std::string ruleImage, bool useAlpha)
{
    if (!TransitionScene::initWithDuration(duration, scene))
    {
        return false;
    }

    this->effect = PatternTransitionLayer::create(ruleImage, useAlpha);
    this->addChild(this->effect, 2, kSceneFade);

    this->effect->setTransitionCompleted([this](PatternTransitionLayer* t)
    {
        if (this->doneFadeOut)
        {
            this->finish();
            return;
        }

        this->hideOutShowIn();
        this->effect->beginTransition(PatternTransitionLayer::TransitionType::FadeIn, 
                this->getDuration() / 2.0f);
        this->doneFadeOut = true;
    });

    return true;
}

void TransitionPattern::onEnter()
{
    TransitionScene::onEnter();
    this->_inScene->setVisible(false);
    this->effect->beginTransition(PatternTransitionLayer::TransitionType::FadeOut, this->getDuration() / 2.0f);
}

void TransitionPattern::onExit()
{
    TransitionScene::onExit();
    this->removeChildByTag(kSceneFade, false);
}

Cocos2d-xのフレームワークにゆるく乗っていくだけなので仕組みさえわかれば簡単かと思います。

そのほかのコード

上記コード中で使用している未説明のコードは以下の通りです。

CreateFuncクラス

各C++クラスが継承している、CreateFunc は以下を参照ください。

CRTPイデオムを使用したCreateFuncの記述の簡易化になります。

http://melpon.org/blog/cocos2dx-create-func

CommonFunction.hppについて

CommonFunction.hppにはグローバル汎用処理を記述しています。

上記コード中で使用したメソッドの実装は以下の通りです。

// CommonFunction.hpp

// 幅を取得する
static float __WIDTH(cocos2d::Node* node)
{
    return node->getContentSize().width;
}

// 高さを取得する
static float __HEIGHT(cocos2d::Node* node)
{
    return node->getContentSize().height;
}

// チェックしてあれば実行する
template<class Fn, typename... Args>
static void invoke_fn(Fn& f, Args&&... args)
{
    if (f)
    {
        f(std::forward<Args>(args)...);
    }
}

今回はコードが以上に長くなってしまいましたがこれでUnityと同じような演出が実行できます。

全部コピペで動くはずなのでよかったら使ってみてください。

参考

今回ググりまくってしまいました。以下サイト感謝です。ありがとうございます。

シェーダー沼は深そうでした。

tsubakit1.hateblo.jp

brbranch.jp

www.slideshare.net

secondflush2.blog.fc2.com

glslsandbox.com

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