Cocos2d-x v3でShaderをいじってスプライトの輝度や明度を操作する

Pocket

brightness01

タイトルの通りです。
以前、blendFuncともう一つのスプライトを使って擬似的に輝度と明度を操作する方法を投稿したことがあるのですが、もっとちゃんと輝度と明度を操作したいと思うようになったのでその方法を調べてまとめました。
ついでに調べてる中でOpenGLがやってることがちょっとだけわかってきたのでそれも備忘として載せてます(まだOpenGLの知識は初心者に毛が生えそうで生えない程度なので、間違いがありましたらご指摘下さい)。

なお、シェーダーについては以下をとても参考にしました。

【TechBuzz】第9回cocos2d-x勉強会「シェーダ書いてますか?」
http://www.slideshare.net/nyagasuki/ss-44161092

とりあえずサンプル

cocos2d-xはOpenGLを使って画面を描画しているんで、結論から言えばShaderをいじることで何でもできちゃうみたいです。輝度も明度もShaderを自作することで無事できました。

以下、サンプルの動画です。

そう、これがしたかったんですよ(`・ω・´)
一応サンプルソースも以下に載せました。

cocos2d-xのv3でスプライトの輝度を変えるサンプル
https://github.com/brbranch/cocos2dxv3Brightness

中のサンプルソース内に「ChangeColorBy」というアクションクラスも作成しています。使い方はGitHub内に簡単にですがまとめました。
テキトーなコードで恐縮ですがshaderフォルダをcocos2d-xプロジェクトに入れることで使えるので使いたい方がいましたらご自由にお持ち帰り&加工修正下さい。

※7/25(月)追記
どうも、ShaderはSpriteBatchNodeを利用しているとちゃんと機能しないようです(´・ω・`)
使える範囲は限られちゃいますね。

というか、どうしてcocos2d-xには輝度と明度を変えるアクションやメソッドがないんだろ(´・ω・`) もしかしたらあるけどぼくが見つけられてないだけなのかな。。

Cocos2d-xの色の扱い

さて、ここからおまけです(Shaderについての備忘メモです)。
cocos2d-xのNodeには、一応「Node::setColor」というメソッドが存在しているし、徐々に色を変えていくアクションとして「TintBy」「TintTo」というのも存在しています。

殆どの場合はこのメソッド(アクション)を使うことで色の変更は事足りるでしょう。ただこれらは両方とも「彩度」「明度」「輝度」を下げることはできますが、上げることができません
理由は簡単で、cocos2d-x内で使っているデフォルトのシェーダーの指定がそうなってるんですね。つまりRGB(255,255,255)が画像本来の色と定義されており、それ以上の値を指定することができないため明度も輝度も上げることができないんですね。

brightness02

ちなみに、Cocos2d-xの内部的にはShaderで本来の色と指定した色を掛けることで実現しているみたいです。
OpenGLでは全ての色を-1.0〜1.0の間で指定するそうなのですが、Shaderでは本来の色に255を1.0に変換した値を掛けるようにしています。だからどうやっても本来の画像以上の明度、輝度にはならないんですね。

Shaderを実装する

本来の画像よりも輝度と明度を変更したい場合は二つの方法があります。
(もしかしたら既にcocos2d-xに実装されてるけどぼくが知らないだけかもだけど)
一つは以前にも紹介した「同じ画像を重ねてblendFuncをいじる」という方法です。
変なやり方ですけど、OpenGLの描画方法を見る限りあながち間違えの方法じゃなかったっぽい。

そしてもう一つがShaderを自作するという方法です。
これ、とても敷居が高そうに思えたので敬遠してたのですが、やってみると思ってたよりは簡単にできました。
食わず嫌いダメね。

Shaderの実装方法

バーテックスシェーダとフラグメントシェーダを用意する

まず、OpenGLのShader用のスクリプトファイル(2つ)を用意します。
バーテックスシェーダとは、ざっくり言えば図形情報を定義するファイルです。要は「どこに表示するか」の定義でしょうか。Spriteで実装する場合は基本画像を利用するはずなので、書く内容はどれも殆ど同じになります。

// cocos2d-x側で自動で入れてくれる情報
attribute vec4 a_position;
attribute vec2 a_texCoord;
attribute vec4 a_color;
   
#ifdef GL_ES
varying lowp vec4 v_fragmentColor;
varying mediump vec2 v_texCoord;
#else
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
#endif

void main() {
    // 頂点データの設定
    gl_Position = CC_PMatrix * a_position;
    // フラグメントシェーダに渡す色情報 (setColorで設定した値)
    v_fragmentColor = a_color;
    // フラグメントシェーダに渡すテクスチャの情報
    v_texCoord = a_texCoord;
}  

次にフラグメントシェーダとは、簡単に言えば「どのように表示するか」を定義するファイルです。
これはバーテックスシェーダから1ピクセルごとに毎回呼ばれるのか、全ての頂点情報が与えられて一度だけ呼びだされているのかはよくわからないのですが、いずれにしても頂点情報全ての共通処理を書く必要があります。

とりあえず真っ赤に表示されるようにしてみます。

#ifdef GL_ES
precision mediump float;
#endif
// バーテックスシェーダで設定された色情報
varying vec4 v_fragmentColor;
// バーテックスシェーダで設定されたテクスチャ情報
varying vec2 v_texCoord;

// こちら側で独自に定義した変数
uniform vec4 u_color;

void main(void)
{
    // Spriteのデフォルト設定
    // Node::setColorで設定した値(RGBA)とテクスチャ内の色とを掛けている
    // vec4とvec4の掛け算だと、具体的には(R*R , G*G,  B*B , A*A)の計算がされる
    // vec4 c = v_fragmentColor * texture2D(CC_Texture0, v_texCoord);

        // 予約された変数に値を格納することで反映される
    // gl_FragColor = c;

    // 下記の場合は問答無用で全て指定した色にする
    gl_FlagColor = u_color;
}

なお、バーテックスシェーダとフラグメントシェーダについては以下のサイトがとてもわかりやすくて参考になります。

床井研究室
http://marina.sys.wakayama-u.ac.jp/~tokoi/?date=20090827

二つのファイルを読み込む

上記のスクリプトファイルは、Resourcesファイルに入れることでファイルから読み取ることができます。そのためのメソッドもCocos2d-x側で用意されています。たとえば、上記を「Resources/shaders」フォルダ内に入れた場合は以下のようになります。

GLProgram::createWithFilenames("shaders/test.vert", "shaders/test.frag");

もちろんこの使い方でも全然いいと思うのですが、ちょっとResourceファイルに入れるのは不便だなっていう場合は直接コードに埋め込んじゃいます。そうするとライブラリ化もしやすいですし、なんとなくその方が早そうですよね。

さっきのファイルを「Classes/shader/shaders」フォルダ内に持っていき、以下のように書き換えます。
なお、「\n」としてるのは誤字ではなくそのままそう記述します。

/** test.vert */
const GLChar* TestVert = 
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() {
    gl_Position = CC_PMatrix * a_position;
    v_fragmentColor = a_color;
    v_texCoord = a_texCoord;
});

/** test.frag */
const GLChar* TestFrag = 
STRINGIFY(
\n#ifdef GL_ES\n
precision mediump float;
\n#endif\n
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;

uniform vec4 u_color;
void main(void)
{
    gl_FlagColor = u_color;
});

このSTRINGIFYはこの後作るファイルにてマクロ登録します。
つづいて、Shadersクラスを作成します。

/** Shaders.hppファイル */
// 上で定義した名前をここで定義
extern const GLchar* TestVert;
extern const GLchar* TestFrag;

/** Shaders.cppファイル */
#include "Shaders.hpp"
// さっきのSTRINGIFYマクロを登録
#define STRINGIFY(A) #A
// ファイルをインクルードする
#include "shaders/test.frag"
#include "shaders/test.vert"

こうすることで文字列として埋め込むことができ、以下のように呼び出せます。

GLProgram::createWithByteArrays(TestVert,TestFrag);

cocos2d-xでもこんな風にやってるんで、それでいいんじゃないかなきっと。わからないけど。

変数を設定して適応する

次に、シェーダをSpriteに適応します。
他のサイトではAttribute(属性)なども指定しているのもありますが、下記の方法だとcocos2d-x側が設定してくれるのでおすすめです。

// シェーダーの作成
auto s = GLProgram::createWithByteArrays(TestVert , TestFrag);
auto state = GLProgramState::getOrCreateWithGLProgram(s);
// スプライトに設定
sprite->setGLProgramState(state);
// 変数の設定(test.fragで設定した「u_color」内に設定される)
state->setUniformVec4("u_color", Vec4(1.0, 0.0, 0.0, 1.0));

実行するとこんな感じで真っ赤に表示されます。

brightness03

他色々知っといた方がよさそうなこと

以上がシェーダーの基本的な使い方です。
この後単なる赤いだけの子をもうちょっと実用性のあるものに変更しようと思いますが、その前にOpenGLで知っといた方が良さそうなことをさわりだけまとめます。

変数について

シェーダでは以下の変数が使えるそうです。

  • attribute:バーテックスシェーダでのみ設定・使用できる変数
  • uniform:フラグメントシェーダでも設定・使用できる変数
  • varing:バーテックスシェーダがフラグメントシェーダに渡す変数
  • const:定数

vec3,vec4について

こいつら変数名がころころ変わってよくわからないんですが(´・ω・`)
ただ、基本は「x,y,z」「x,y,z,w」を使うぽいです。
でも実は「r,g,b」「r,g,b,a」ともなるし「s,t,p」「s,t,p,q」となることもあります。
「x」「r」「s」は同じ場所を指すそうです。
また「vec.xyz」と指定したり「vec.xxx」や「vec.zzz」と指定したりもできるみたい。

たとえば「vec4 v = vec4(0.1, 0.2, 0.3, 0.4);」となってる場合、
「v.xxx」は「(0.1,0.1,0.1)」と同じです。

ね、よくわからないでしょ?(´・ω・`)

色を加算するシェーダーに改造する

まあ、というわけなのでとりあえず色を掛けるのではなく加算するシェーダーに書き換えてみます。
既に「u_color」という変数は設定できるようにしたので、それを加算するようにしてみましょう。
(読みづらいのでResource内に入れた体で書きます)

#ifdef GL_ES
precision mediump float;
#endif
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
uniform vec4 u_color;

void main(void)
{
    vec4 c = v_fragmentColor * texture2D(CC_Texture0, v_texCoord);
    c.r = clamp(c.r + u_color.r , 0.0, 1.0);
    c.g = clamp(c.g + u_color.g , 0.0, 1.0);
    c.b = clamp(c.b + u_color.b , 0.0, 1.0);
    gl_FlagColor = c;
}

clampは、第一引数が第二引数未満の場合第二引数が、第三引数以上の場合第三引数が、それ以外の場合第一引数が返される関数です。要は0以上1未満にしようとしてます。

この状態で実行したのが以下の左側になります(赤だけ1.0追加してる)。
ついでなので、通常のNode::setColor(Color3B(255,0,0));を実行したのも右に表示させます。

brightness04

右側とくらべて明らかに明るさがプラスされてますね。
ただ、左側は本来透明であるはずの部分も赤色になっています。これはなぜでしょうか。

理由はよくわからない

すみません(´・ω・) 最初はBlendFuncのせいかなーとか考えてたんですけど、それだと矛盾がでてたのでよくわからないです(´・ω・)
ただ、cocos2d-xではOpasityをColorとは別で持ってるんで、もしかしたらそれが原因なのかなぁ?

とりあえず、以下のようにすると透明である部分は透明のままになります。

// main部分のみ抜粋
void main(void)
{
    vec4 c = v_fragmentColor * texture2D(CC_Texture0, v_texCoord);
    c.r = clamp(c.r + u_color.r , 0.0, 1.0) * c.a;
    c.g = clamp(c.g + u_color.g , 0.0, 1.0) * c.a;
    c.b = clamp(c.b + u_color.b , 0.0, 1.0) * c.a;
    gl_FlagColor = c;
}

brightness05

うん、いい感じですね。

まとめ

とまあ、こういう感じでシェーダーを使えるようになるとCocos2d-xでの表現幅も増えていきそうですね。
軽度と明度を操作するサンプルも、上のができるようになった後は結構簡単に実装できました。

最後に、OpenGLでブラウザ上からシェーダーをごにょごにょできるサイトもあったので、それも紹介しておきます。

GLSL Sandbox
http://glslsandbox.com/

良いOpenGLライフを(^-^)/
ぼくはやりたいことはできたんで、しばらくはいじりません。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です