日記

cocos2d-x v3.0 シェーダを使ってみる

シェーダなんて今まで触ったことないような私ですが、
使ってみたくなったので挑戦してみました。
ちゃんと理解してない状態で書いてるんで、間違ったこと言ってるかもしれません。
こんな感じで書けば一応動くんだなぁ程度の気持ちでご覧ください。

今回やってみるのはモザイクです。
OpenGLのシェーダはGLSLという言語で書くそうです。
そして、頂点シェーダとフラブメントシェーダの2つのファイルが必要です。
細かいことはあんまりよくわかってないですけど、適当にググりながら試行錯誤したら↓のコードが出来上がりました。

mosaic.vsh

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_PMatrix * a_position;
    v_fragmentColor = a_color;
    v_texCoord = a_texCoord;
}

mosaic.fsh

varying vec2        v_texCoord;
uniform sampler2D   u_texture;
uniform vec2        u_texSize;
uniform int         u_mosaicLevel;

void main() {
	vec4 color;
	if(u_mosaicLevel > 1){
    		vec2 target;
    		target.x = float(int(v_texCoord.x / float(u_mosaicLevel) * u_texSize.x + 0.5)) * float(u_mosaicLevel) / u_texSize.x;
    		target.y = float(int(v_texCoord.y / float(u_mosaicLevel) * u_texSize.y + 0.5)) * float(u_mosaicLevel) / u_texSize.y;
		color = texture2D(u_texture, target);
    	}else{
    		color = texture2D(u_texture, v_texCoord);
	}
    	gl_FragColor = color;
}

cocos2d-xで使う部分。

#define VISIBLE_SIZE	cocos2d::Director::getInstance()->getVisibleSize()
auto sp = Sprite::create("hoge.png");
sp->setPosition(VISIBLE_SIZE.width / 2, VISIBLE_SIZE.height / 2);

auto shader = new GLProgram();
shader->initWithFilenames("mosaic.vsh", "mosaic.fsh");
shader->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_POSITION, GLProgram::VERTEX_ATTRIB_POSITION);
shader->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_TEX_COORD, GLProgram::VERTEX_ATTRIB_TEX_COORDS);
shader->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_COLOR, GLProgram::VERTEX_ATTRIB_COLOR);
shader->link();
shader->updateUniforms();
shader->setUniformLocationWith1i(shader->getUniformLocationForName("u_mosaicLevel"), 10);
shader->setUniformLocationWith2f(shader->getUniformLocationForName("u_texSize"), VISIBLE_SIZE.width, VISIBLE_SIZE.height);
sp->setShaderProgram(shader);

this->addChild(sp);

とりあえずこれで画像にモザイクをかけることができました。
細かい解説できるほど理解してないので、詰まった部分だけざっくりと
mosaic.vshの

gl_Position = CC_PMatrix * a_position;

という部分ですが、調べてみると”CC_PMatrix”ではなく、”CC_MVPMatrix”を使っていました。
しかし、それだとスプライトの位置がずれちゃったのでこうしたらうまくいきました。具体的にどういう処理をしているか理解してないので、わかったら追記します。

mosaic.fshでモザイクの処理を書いています。
v_texCoordには、テクスチャの描画する座標が入っています。

color = texture2D(u_texture, v_texCoord);

でテクスチャからその座標の色を取得し、それを返すことで描画されるっぽいです。(たぶん)
この座標を、ある数で割って、四捨五入して、同じ数をかけてあげればモザイクになるという寸法です。(わかりにくい)
四捨五入は0.5を足してからintにキャストしちゃえばOKです。別に四捨五入じゃなくて切り捨てでもいいんですが、なんとなく。

target.x = float(int(v_texCoord.x / float(u_mosaicLevel) * u_texSize.x + 0.5)) * float(u_mosaicLevel) / u_texSize.x;

この部分、floatでキャストしまくってますね。
Windows上では、こんなことする必要なかったんですが、Androidではこうしないと動きません。
スマホだとGLSL ESを使っていて、それだと暗黙変換ができないらしいです。

cocos2d-xでこれを使うには、まず、mosaic.vshとmosaic.fshをResourceフォルダの中に入れます。
そして、GLProgramクラスで設定を読み込んで、それをスプライトにセットすれば使えます。
GLSLで書いたプログラムは単なるテキストファイルで、実行時にコンパイルされるらしいです。(あってる?)

shader->initWithFilenames("mosaic.vsh", "mosaic.fsh");

の部分で読み込んでます。中身の文字列を読めればいいので、拡張子とか変えても大丈夫そうです。
FileUtilesのgetData関数を使ってデータの読み込みをしているので、以前の記事で書いた方法を使えば暗号化もできます。
また、最終的にデータを読み込んだ後は
GLProgram::initWithByteArrays(const GLchar* vShaderByteArray, const GLchar* fShaderByteArray)
に値を渡しているので、c++中にGLSLのコードを直接ハードコーディングしちゃっても動きそうですね。やりませんけど。

shader->setUniformLocationWith1i(shader->getUniformLocationForName("u_mosaicLevel"), 10);

ここでGLSL中の変数に値を渡しています。このコードでは”u_mosaicLevel”の値を変えることでモザイクの粗さをコントロールできます。
今回はsetUniformLocationWith1i()とsetUniformLocationWith2f()を使っていますが、似たものとして
*1i ~ *4i
*1f ~ *4f
があります。(名前長いんで省略)これらはそれぞれ
int, ivec2, ivec3, ivec4
float, vec2, vec3, vec4
に対応しているっぽいです。
この値は、後から変更することが出来ます。
その際は、”updateUniforms()”をセットで使う必要があります。


シェーダを使えばラスタースクロールとか色々出来そうで楽しそうですね。

2014/03/30


cocos2d-x v3.0rc0をWindowsで使う&変更点

v3.0rcになって、プロジェクトの作成方法が大きく変わったので紹介します。
今までは「tools/project-creator/create_project.py」を実行すると、GUIで設定して作成できましたが、これがなくなっています。
代わりに「cocos2d-x-3.0rc」のフォルダ直下にsetup.pyがあります。
どうやらこれを実行するとNDK_ROOTの設定とかを自動でやってくれるらしいです。
設定が終わると、コマンドプロンプト上で「cocos new “My Game” -l cpp -p org.cocos2d.mygame 」のように打つことでプロジェクトを作成できるそうです。
……なのですが、できませんでした。
cocosというコマンドを使うためには、「任意の場所\cocos2d-x-3.0rc\tools\cocos2d-console\bin」を環境変数のPathに追加する必要があります。
setup.pyでそこまでやってくれるのかと思ってましたが、そういうわけでもなかったようです。(自分の環境が悪いんでしょうか?)
ともかく、手動で環境変数を弄ることでcocosコマンドを使えるようになりました。
で、ここでまた問題が出まして、cocos2d-x本体をマイドキュメントの下に置いてたんですが、それだとパスに「My Document」と空白が出来てしまい、動いてくれませんでした。Cドライブの直下に置けば問題ないです。
「cocos new “My Game” -l cpp -p org.cocos2d.mygame 」
↑のコマンドを実行するとカレントフォルダに「My Game」というプロジェクトが出来ます。「-l」はlanguageで、「-p」はpackageのことです。


また、rc0になってbetaから大きく変わったことがあるので、私が気づいた分だけご紹介。

・EGLViewがGLViewに変更

クラス名の変更です。

・GLViewの設定をする場所が、各プラットフォームのmain関数からAppDelegateへ変更

割りと大幅な変更です。以前は、ウィンドウのタイトルとかサイズをプラットフォーム毎に設定する必要がありましたが、AppDelegateで一括してやるようになりました。ウィンドウサイズを指定しても、Androidなどのスマホでは反映されないようです。

・FadeIn/FadeOutが、FadeToを継承するクラスに変更

以前は透過度が0から255/255から0になるアクションでしたが、スタート時の透過度は現在の値を使うようになりました。
新しく生成したスプライトを事前に透過度を0にせずに、FadeInとか多様してたので、ちょっと修正する必要がでました。
でも、挙動としてはこっちの方が自然ですよね。

・LabelBMFontがLabelを内包するように変更

一つ前の記事にも追記しましたが、Labelでビットマップフォントもttfも使えるようになりました。

とりあえずこんな感じです。他にも何か気づいたら追記します。

2014/03/17


cocos2d-xでドット絵をくっきり表示させる方法

最近はスマホゲーでもドット絵が流行ってたりするようですね。
でも、たまーにドット絵が売りなのにぼやけちゃってるのがあったりして残念な気持ちになったりするのです。
(昔のゲームはブラウン管でぼやけて表示されていたので、それを再現しているのかもしれませんが)

cocos2d-xも普通にドット絵を表示するとぼやけちゃいます。

auto sprite = Sprite::create("dot.png");
sprite->setPosition(0, 0);
sprite->getTexture()->setAliasTexParameters();
this->addChild(sprite);

こんな感じでOKです。
Texture2Dの”setAliasTexParameters()”を呼んであげるだけです。
これはスプライトではなく、テクスチャの設定なので、同じテクスチャを使いまわす場合は一回設定するだけで大丈夫です。
また、テクスチャアトラス(スプライトシート)を使う場合もすべてのスプライトフレームに反映されます。
ビットマップフォントに対しても同じように出来ます。

auto label = LabelBMFont::create("hoge", "font.fnt");
label->setPosition(0, 0);
label->getTexture()->setAliasTexParameters();
this->addChild(label);

2014/3/16 追記
v3.0rc0で試したところ、LabelBMFontにgetTexture()できなくなってました。
LabelBMFontの作りが大幅に変わって、中にLabelを持つ形に変更したようです。
で、元々のLabelBMFontの機能はLabelTTFとかとまとめてLabelに統合されたっぽいです。
これからLabelBMFontは非推奨になるんでしょうかね?
変更後のソースはこちらです。

auto label = Label::createWithBMFont("font.fnt", "hogehoge");
label->setAnchorPoint(Point(0.5, 0.5));
label->setPosition(POINT_C(0, 0));
label->getTexture()->setAliasTexParameters();
this->addChild(label);

引数の順番に注意です。フォントのファイル名が先になってます。
あと、LabelBMFontのアンカーポイントは中心でしたが、Labelは左下になるようです。
rc1で中心に修正されました。


せっかく技術的なこと書こうとしているのに、コメント欄がないのも如何なものかと思い、復活させてみました。
たぶん2年ぶりくらいかな。
自前で作った掲示板はスパムだらけで対応がめんどくなって撤去しちゃったので、こっちも二の舞いにならないといいなー。

2014/03/13


cocos2d-x v3.0 Windowsで日本語を表示する

先日、cocos2d-x v3.0rcが公開されましたね。
プロジェクトの作成方法が大幅に変わったようです。
Macですが、早速記事を書いている方がいたのでリンク貼っときます。
Cocos2d-x3.0rcで新規プロジェクト作成してandroid実機で動かす – きょこみのーと


今回はWindowsで日本語の表示をします。
cocos2d-xはLabelTTFとかLabelBMFontで文字の表示をできます。
日本語の表示もちゃんとできます。(Macでは)
Windowsだと文字化けしちゃいました。
どうやら、文字コードがShift-JISになっているのが問題で、UTF-8に変換しなくちゃいけないみたいです。
↓のサイトのコメント欄が参考になりました。
cocos2d-xでWindowsアプリ開発環境も作ってみた » はにらぼ☆てっく

Shift-JISをUTF-8に変換するコードをとりあえず書いてみました。

char*	MultiByteToUTF8(const char* src){
	unsigned int sizeWide = MultiByteToWideChar(CP_ACP, 0, src, -1, nullptr, 0);
	wchar_t* bufferWide = new wchar_t[sizeWide+1]();
	MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, src, -1, bufferWide, sizeWide);
	unsigned int sizeUtf8 = WideCharToMultiByte(CP_UTF8, 0, bufferWide, -1, nullptr, 0, nullptr, nullptr);
	char* bufferUtf8 = new char[sizeUtf8+1]();
	WideCharToMultiByte(CP_UTF8, 0, bufferWide, -1, bufferUtf8, sizeUtf8, nullptr, nullptr);
	delete bufferWide;
	return bufferUtf8;
}

Shift-JISをUTF-8に直接変換はできないので、一旦ワイド文字列に変換しないといけないらしいです。
上から順にざっと説明すると、
まずワイド文字列に変換するのに必要なサイズを取得し、(2行目)
ワイド文字列のバッファを作って、(3行目)
Shift-JISをワイド文字列に変換します。(4行目)
そして、UTF-8に変換するのに必要なサイズを取得し、(5行目)
UTF-8のバッファを作って、(6行目)
ワイド文字列をUTF-8に変換します。(7行目)

と、こんな感じの関数を用意しとくといいと思います。
MultiByteToWideCharとWideCharToMultiByteについては↓を参考にしました。
MultiByteToWideChar – Windows APIの部屋
WideCharToMultiByte – Windows APIの部屋

auto label = LabelTTF::create(Helper::MultiByteToUTF8("ほげ"), "Meiryo", 24);
label->setPosition(0, 0);
this->addChild(label);

使い方はこんな感じです。

auto label = LabelBMFont::create(Helper::MultiByteToUTF8("ふが"), "font.fnt");
label->setPosition(0, 0);
this->addChild(label);

ビットマップフォントの場合はこうなります。

ついでに、ビットマップフォントの作成について。
ShoeBoxという、大変素晴らしいフリーソフトがあったので、試しに使ってみたのですが、結果から言えば日本語で使うにはちょっと問題があります。
使い方をざっくり言うと、
まず作成するフォントの設定をして、使う文字をクリップボードにコピーする。
フォトショ等の画像編集ソフトで文字列をペーストし、レイヤー効果で装飾をして書き出す。
画像をShoeBox上にドロップすると、画像上の透過部分を元に自動で文字を切り分けて、ビットマップフォントを作成する。

文字の装飾をツール上で行わず、他のソフトに丸投げしてくれているおかげで、自由に装飾ができるのです。
しかし、文字の切り分けを自動で行うために、「い」が左右で分裂されてしまうというハプニングが……。
最初に分裂しそうな文字を加工して繋げておいて、ビットマップフォント生成後に修正とかすれば良さそうですが、ちょっとめんどくさいですね。
ShoeBoxには他にもスプライトシートを作る機能もあるので、そっちで使っています。

文字に装飾をする必要がないのであれば、Bitmap Font Generatorで十分だと思います。
装飾する場合は、あらかじめ余白ができるように生成して、出来た画像にフォトショのレイヤー効果でなんとかなりそうですね。(グラデーションは難しそうですが)

あと、cocos2d-xでビットマップフォントを使う場合の注意点として、一つのフォントに画像は1枚しか使えないようです。
使用する文字が多い場合は大きな画像にせざるを得ないですが、あんまり大きすぎるのは良くないので、文字を小さめにするとか使う漢字を最小限にするとか対処する必要がありそうですね。
ちなみにAndroidかiOSの場合は2048*2048が上限だったような気がします(うろ覚え)

2014/03/12


cocos2d-x v3.0betaでマウスイベントを取得する

今回はマウスイベントについてです。
v3.0ではエンジン側をいじることなく、普通に取得できます。(2.xでは試してないけど、無理だよね?)

とりあえず、ソースコードどーん。

//画面の拡大率
#define EGL_SCALE	cocos2d::Director::getInstance()->getOpenGLView()->getScaleX()

bool HelloWorldScene::init(){
	if ( !Layer::init() ) return false;
	//カーソルのスプライト
	auto cursor = Sprite::create("cursor.png");
	this->addChild(cursor);

	//マウスイベント設定
	auto mouseListener = EventListenerMouse::create();
	mouseListener->onMouseMove = [=](Event* event){
		auto mouse = (EventMouse*)event;
		cursor->setPosition(Point(mouse->getCursorX(), mouse->getCursorY()) / EGL_SCALE);
	};
	_eventDispatcher->addEventListenerWithSceneGraphPriority(mouseListener, this);

	return true;
}

これで、マウスの位置に画像が表示されます。

	auto cursor = Sprite::create("cursor.png");
	this->addChild(cursor);

カーソルの画像からスプライトを生成して、レイヤーにaddします。ここはとくに説明する必要はないですよね?

	auto mouseListener = EventListenerMouse::create();

EventListenerMouseを生成します。cocos2d-x v3.0からはC++11なので、auto型が使えます。
auto型っていうのは、コンパイル時に型を推測して変換されるものです。
C++0xのautoキーワードによる型推論 – ぬいぐるみライフ(仮)
↑に色々と詳しいことが書いてます。

	mouseListener->onMouseMove = [=](Event* event){
		auto mouse = (EventMouse*)event;
		cursor->setPosition(Point(mouse->getCursorX(), mouse->getCursorY()) / EGL_SCALE);
	};

「onMouseMove」に関数を代入します。C++11なので、ラムダ式使えます。
C++0x ラムダ式 – Faith and Brave – C++で遊ぼう
ラムダ式については↑とか参考にしました。

「onMouseMove」はマウスが動いた時に呼ばれます。文字通りですね。
マウスの座標などの情報はEvent*型で渡されるのですが、マウス関連の情報はEventMouse*型にあるので、キャストします。
そしたら、getCursorX()、getCursorY()で座標を取り出せます。ただし、これは画面上での座標であって、OpenGLで描画される空間での座標ではありません。
描画サイズを全く弄ってないなら、このままでも問題ないのですが、全画面とか対応するときに困ります。
描画サイズを画面に合わせて変更するには、EGLViewのsetDesignResolutionSize()を使います。

bool AppDelegate::applicationDidFinishLaunching() {
    // initialize director
    auto director = Director::getInstance();
    auto eglView = EGLView::getInstance();

    director->setOpenGLView(eglView);
    
    //描画サイズの調整
    eglView->setDesignResolutionSize(600, 800, ResolutionPolicy::FIXED_HEIGHT);

    // create a scene. it's an autorelease object
    auto scene = HelloWorldScene::createScene();

    // run
    director->runWithScene(scene);

    return true;
}

こんな感じです。(解説に関係ない部分ごっそり削ってます)
これに関しては、↓とか参考になると思います。(v2.xですが、使い方は大体一緒です)
がおまる開発ブログ 解像度の設定:setDesignResolutionSizeを使用する

#define EGL_SCALE	cocos2d::Director::getInstance()->getOpenGLView()->getScaleX()

話を戻しまして、描画するサイズを変えたので、それを取得します。
getScaleX()で、X軸でのスケールを取ってます。アスペクト比は変えていないので、Xだけ見れば大丈夫(のはず)

		cursor->setPosition(Point(mouse->getCursorX(), mouse->getCursorY()) / EGL_SCALE);

そして、そのスケールで座標を割ってあげると、OpenGL上での座標になります。
v2.xでは、演算子のオーバーロードがありませんでしたが、v3.0では使えます。便利!

とまぁ、こんな感じです。
ただこれ、マウスが動いた時の座標はわかるけれども、最初に動かす前の座標がわからないんですよね。
今のところ特に困ってないので、いいのですが、何か方法あるのかなぁ。
ちなみに、onMouseMove以外にも、
onMouseDown
onMouseUp
onMouseScroll
があります。説明せずとも名前で分かりますよね。
Down/Up時のクリックされたボタンの判定はEventMouseのgetMouseButton()で行います。
int型で、それぞれのボタンはCCEventMouse.hに

#define MOUSE_BUTTON_LEFT       0
#define MOUSE_BUTTON_RIGHT      1
#define MOUSE_BUTTON_MIDDLE     2
#define MOUSE_BUTTON_4          3
#define MOUSE_BUTTON_5          4
#define MOUSE_BUTTON_6          5
#define MOUSE_BUTTON_7          6
#define MOUSE_BUTTON_8          7

こんな感じで定義されてます。

以上。次回は何にしよう……。

2014/02/24


≪前  |  次≫