Tuesday, March 29, 2016

OpenGLでDirectX風に頂点レイアウトを書く方法

以前の記事で、OpenGLの頂点レイアウトやInput Layout Qualifiersの事を書きましたが、もうちょっといい方法を思いついたので書きます。以下のようにDirectX11のように頂点レイアウトを構造体の配列として書きます。

この方法のキモは、elementsの配列の並び順にglBindAttribLocationで0から順に番号を割り当てることです。この例では、"POSITION"が0番、"COLOR"が1番、"TEXCOORD"が2番です。例えば以下のような頂点シェーダのための定義になります。inのところだけ見てください。glBindAttribLocationを使うので当然Input Layout Qualifiersは不要です。

この過程を同じ頂点レイアウトを使用する複数のシェーダーに適用することで、シェーダが変わっても同じVAO(Vertex Array Object)を使用できます。言い換えると、シェーダーからglGetAttribLocationで"POSITION"は何番か、と問い合わせる方法でVAOを生成してしまうとそのシェーダ専用のVAOしか作れなくなってしまう為、InputElementという仲介役を立てたと言えます。

以上を踏まえたVAOを生成する実装の実例を紹介します。長いですがやってることはInputElementの配列を解釈しながらVAOを作っているだけです。こうして作ったVAOは同じInputElement配列を指定して作った頂点シェーダならどれにでもバインドできるようになります。

afCreateVAOは、InputElement構造体の配列、インデックスバッファ、頂点バッファの配列(それぞれ順にInputElement構造体のslotに対応、OpenGLにslotという概念は無いが、DirectX風に定義)、strideの配列を受け取ってVAOを作ります。

ほぼ同じ引数を受け取るafSetVertexAttributesは、頂点バッファを現在のVAOにバインドする部分です。VAOの無いOpenGL ES 2.0でもそのまま使えるコードです。

こうすることでInput Layout Qualifiersを使う必要もなくなり、glGetAttribLocationも必要なくなり、OpenGL ES 2.0、OpenGL ES 3.0双方で同じコードが動き、DirectXとのコードの共通化にも役立ちます。

Saturday, February 6, 2016

DirectXとOpenGLとrow majorとcolumn majorのまとめ

DirectXの行列は、移動が4行目に来ます。
   

対して、OpenGLはちょうどDirectXの行列を転置したもので、移動が4列目に来ます。
 

ところが、行列をfloat型16個の配列として見てみると、両者のメモリレイアウトは全く同じです。





例えばXMMatrixLookAtRHで生成したビュー行列をgluLookAtと置き換える正常に動作します。OpenGLはcolumn major(列優先)で、DirectXはrow major(行優先)なので、数学的に1回、メモリレイアウトで1回、2回転置してメモリレイアウトが同じになったと見ることもできます。

では両者は同じかと言うと、数学的な定義とメモリレイアウトは別の話なので注意が必要です。頂点v、ワールド(モデル)行列W、ビュー行列V、射影行列Pの計算式はOpenGLとDirectXで逆の並びになっています。

DirectX:

OpenGL:


この並びはシェーダーを書く時の慣習にもなっていて、多くの参考書はこの順で書かれているはずです。両者のメモリレイアウトは同じはずなので、掛け算の順番が違うということは、GLSLはcolumn major、HLSLはrow majorの並びと推測できます。

ところが、GLSLもHLSLもcolumn majorがデフォルトになっています。OpenGLはC/C++側もGLSLもcolumn majorなので良いのですが、DirectXはC/C++側がrow major、HLSLがcolumn majorと異なっています。

しかし、これはおかしいです。row majorではないと、

として正しい結果が得られないはずだからです。どうも、DirectXはシェーダに行列を渡す時点で転置させる習慣があったようで、結果としてこの順で掛け算して正しくなっていました。おそらく、DirectX9時代はD3DXあたりで勝手にやってくれたのでプログラマが気にする必要が無かったのだと思います。

しかし、DirectX11はConstant Bufferの配置までプログラマに委ねられているので、理解して使う必要があります。実は、DirectX11ではOpenGLと同じように、

の順で掛けると正しい結果になります。あるいはHLSLで行列を宣言するときにrow_majorを指定すると、

この従来の順で計算しても正しくなります。

Saturday, January 9, 2016

続・Visual StudioからAndroid開発を実践してみた

Visual Studio 2015からAndroidの開発をしてみましたが、思わぬエラーに遭遇して悩みました。そんな経験を書き連ねてみます。


Clangで内部エラー


Visual Studio 2015でのデフォルトのコンパイラがClangなのですが、特定のソースファイルをコンパイルする時に以下のような内部エラーを発生させるようです。
1>  This application has requested the Runtime to terminate it in an unusual way.
1>  Please contact the application's support team for more information.
1>  Assertion failed!
1>
1>  Program: C:\ProgramData\Microsoft\AndroidNDK\android-ndk-r10e\toolchains\llvm-3.6\prebuilt\windows\bin\clang.exe
1>  File: /s/ndk-toolchain/src/llvm-3.6/llvm/tools/clang/lib/AST/DeclBase.cpp, Line 1299
1>
1>  Expression: DeclKind != Decl::LinkageSpec && "Should not perform lookups into linkage specs!"
1>clang.exe : error : clang frontend command failed with exit code 3 (use -v to see invocation)
1>  clang version 3.6
1>  Target: armv7-none-linux-androideabi
1>  Thread model: posix
1>  clang.exe: note: diagnostic msg: PLEASE submit a bug report to http://source.android.com/source/report-bugs.html and include the crash backtrace, preprocessed source, and associated run script.
1>  clang.exe: note: diagnostic msg:
1>  ********************
1>
1>  PLEASE ATTACH THE FOLLOWING FILES TO THE BUG REPORT:
1>  Preprocessed source(s) and associated run script(s) are located at:
1>  clang.exe: note: diagnostic msg: C:\Users\Jin0103\AppData\Local\Temp\bvh-1eb1f7.cpp
1>  clang.exe: note: diagnostic msg: C:\Users\Jin0103\AppData\Local\Temp\bvh-1eb1f7.sh
1>  clang.exe: note: diagnostic msg:
1>
1>  ********************

回避方法が無さそうなので、やむを得ずGCCに切り替えました。プロジェクトのプロパティのPlatform Toolsetで変更可能です。



libmのリンク


Visual Studioではsin, cos等の数学ライブラリは明示的にリンクしなければ以下のようなリンクエラーを出すようです。

1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(51): error : undefined reference to 'sin'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(56): error : undefined reference to 'cos'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(61): error : undefined reference to 'tan'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(66): error : undefined reference to 'asin'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(71): error : undefined reference to 'acos'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(78): error : undefined reference to 'atan2'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(109): error : undefined reference to 'floor'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(120): error : undefined reference to 'ceil'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(138): error : undefined reference to 'fmod'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(157): error : undefined reference to 'ceil'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(157): error : undefined reference to 'floor'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(167): error : undefined reference to 'sqrt'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(183): error : undefined reference to 'log'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(186): error : undefined reference to 'log10'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(187): error : undefined reference to 'log'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(187): error : undefined reference to 'log'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lmathlib.c(194): error : undefined reference to 'exp'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lobject.c(103): error : undefined reference to 'pow'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lobject.c(104): error : undefined reference to 'floor'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lobject.c(108): error : undefined reference to 'fmod'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/ltable.c(92): error : undefined reference to 'floor'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/ltable.c(103): error : undefined reference to 'frexp'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lvm.c(858): error : undefined reference to 'fmod'
1>D:\github\AdamasF\VisualStudio\..\submodules\lua\src/lvm.c(883): error : undefined reference to 'pow'

Androidのライブラリはファイル名が全てlibGLESv3, libOpenSLESのようにlibで始まるのですが、libを省略してGLESv3, OpenSLESのように指定する習慣があるようです。数学ライブラリはlibmなので、mとだけ指定するとリンクできます。


Precompiled Headerは切ってみる


なぜ発生するかわからないコンパイルエラーが出るときは、Precompiled Headerを切ってみます。(デフォルトで使うようになっている)


GCCのプリコンパイル済みヘッダはWindows+Visual Studioのものと仕組みが違います。

Windows+Visual Studioでは、例えばstdafx.hを各ソースの最上位でincludeすることを強制して重複する作業を省略しようとします。

GCCでは、各ソースの最上位で強制的にpch.hをincludeしたことにしてしまうようです。

個人的な経験なので参考にならないかもしれませんが、なぜかCであるLuaのソースコードで__cplusplusが定義されたり、Luaのソースコードで最上位で定義されなければならないLUA_COREやLUA_LIB等のマクロ定義に先駆けてlua.hがincludeされたことになっていました。

ぱっと見で分かり難いエラーの原因がプリコンパイル済みヘッダの間違った使い方であったというのはありがちなシチュエーションなので、仕組みを理解したうえで適用するか決めるのがよさそうです。

Sunday, January 3, 2016

Visual StudioからAndroid開発を実践してみた

実際に開発を進めてみるとハマることが多く、メモを残します。

assetsやresフォルダの扱い


Android Studioのようにエクスプローラー上にassetsやresフォルダがあっても自動的に取り込まれません。


Visual Studio上で明示的にassetsやresフォルダを作ってファイルを一つ一つ追加します。

ところで、例えばdrawableフォルダを追加する時に既にエクスプローラー上にdrawableフォルダがある場合エラーが発生します。


これは、Visual Studioがプロジェクト上の概念的なフォルダではなく、本当にディスク上にフォルダを生成しようとしているためです。よって、既存のdrawableフォルダを一度別の場所に移動してからVisual Studio上でdrawableフォルダを作るという過程が必要です。

この辺はWindowsアプリを開発しているときと挙動が違ってややこしいです。


Activityが無いアプリの実機へのインストール


Visual Studio上からapkを実機で実行する場合、起動するActivityをプロジェクトのプロパティに指定するようになっています。



ところが、ウィジェットやライブ壁紙の場合起動可能なアクティビティが存在しない場合があります。その際、Ctrl-F5で実行しようとすると以下のようなエラーが発生してインストールすらしてくれません。

Could not retrieve the launch activity from the package (apk)

こういう時は、以下のようにコマンドラインから入力してインストールします。(バッチを作ります)
adb install -r ARM\Release\KiriWidget.apk

Visual Studio 2015から使われるadb.exeは以下のパスにあるようです。必要に応じて環境変数PATHに通しておきます。

C:\Program Files (x86)\Android\android-sdk\platform-tools

Friday, December 25, 2015

OpenSL ESでActivityが非アクティブになった時に音を停止する簡単な方法

前回OpenSL ESでサウンドを再生しましたが、Activityが非アクティブになっても音が鳴り続ける問題がありました。それに対しての簡単な対処法として、バッファを細かく分けてEnqueueしてみました。Enqueueは32768バイト単位でしています。(サイズは適当です)

最終アップデートからの経過時間が0.5秒を超えたらコールバック無視します。この方法の長所は、コールバック一つでループ再生と非アクティブになった時の音停止を同時に実装でき、コードが簡潔になります。Activityから非アクティブの通知を受け取る必要もありません。短所としては、アップデートループで長い処理をすると音が切れてしまうことです。

また、停止した音を再開する処理を盛り込んでいません。ここは、ゲーム毎の実装依存が多いと思いあえてしませんでした。例えば多くの場合は再開すべき音はBGMだけであったりするでしょう。

AndroidのOpenSL ESはコールバックは別スレッドから来るので特に気をつける必要があります。Android特有の実装についてはNDKのdocs/Additional_library_docs/opensles/index.html に説明があるので、一度目を通しておくと良さそうです。

Saturday, December 12, 2015

実践OpenSL ES、AndroidでWAVファイル再生

OpenSL ESによるサウンドエンジンを使い物になるように書いてみます。

まず、気づくのはDirectXを彷彿させるインターフェース型のAPIであることです。多くのメソッドはSLresultで結果を返し、Destroyでリソースを解放して終了します。素直に書くと冗長なコードになりそうですが、まず最初にDirectX開発者にはおなじみの手法を導入します。

SafeDestroyはDestroy呼び出しと同時にnullptrを代入してダングリングポインタにならないようにします。

SLHandleErrorというのは、各種メソッドがエラーコードを返したらソースコード上の行数と関数呼び出しの概要をログに出力するためのマクロです。Direct3D9のサンプルでは"V"という一文字のマクロが多用されていましたが、それです。

SLCallは、インターフェース呼び出しをすっきり書きたいがために書いたマクロです。これを使ってエンジンの初期化と終了を書いてみます。

エンジンの初期化と終了が書けました。

DirectXから来ると戸惑うのが、SLObjectItfとその他インターフェースに分かれていることです。OpenSLでは全てのインスタンスの実体はSLObjectItfであって、何らかのメソッド呼び出しが必要になったらSLObjectItfから追加のインターフェースを取得します。また、SLObjectItfをDestroyするとSLObjectItfから取得したインターフェースも一緒に無効になります。

engineEngineがSafeDestroyではなくnullptrを代入しているのは、engineObjectをSafeDestroyした時に一緒に消滅しているからです。

次は、WAVファイルをロードして"audio player"を作ってみます。

CreateAudioPlayerで、WAVファイルの特性(ステレオ/モノラル、サンプリングレート、ビット数)に合わせた"audio player"を生成します。WaveFormatExはWindows APIにあるWAVEFORMATEX構造体で、この情報からSLDataFormat_PCM構造体を構築します。リファレンス通りに書くとまたコードが長くなりそうですが、例えばSL_SAMPLINGRATE_44_1は44100000と定義されていたりします。samplesPerSecondを1000倍して代入すれば良いわけです。事前定義値は https://www.khronos.org/registry/sles/api/1.1/OpenSLES.h にあります。

次はWAVファイルの再生部です。

Enqueueで再生するバッファを指定します。ループ再生する場合はコールバックを登録して、コールバックからEnqueueを繰り返します。コールバック設定前にSL_PLAYSTATE_STOPPEDを明示するのはステートがSL_PLAYSTATE_STOPPEDではない場合にSL_RESULT_PRECONDITIONS_VIOLATEDエラーが発生するためです。これはPlayメンバ関数が二回目に呼ばれた場合に起こります。

これで、OpenSLの初期化からWAVファイルの再生までができました。ところで、この方法だとActivityが非アクティブになっても音が鳴り続けるという問題があります。この解決は次回書きます。

Saturday, December 5, 2015

Luaのrequireで独自ファイルシステム上のスクリプトをロード

requireでAndroidのAPKのassetsフォルダからスクリプトを読み込みたくて、方法を探ってみました。requireはCのincludeに似た使い方をしますが、requireの実体はモジュール名を引数に取るただの関数です。

dofileとrequireはよく似た関数ですが、いくつか違いがあります。まずrequireは通常拡張子".lua"無しでモジュールを指定します。これを実装してみます。


前回、dofileのファイルシステムを置き換えているので置き換え版のdofileを流用することでLuaだけで書けました。厳密にはpackage.pathを考慮していないという問題がありますが、これだけで用足りることも多いと思います。

ところで、requireで比較的重要なdofileとの違いが、重複requireガードです。これはpackage.loadedテーブルにロード結果が記録される事で行われます。実際にテーブルの中身をprintで出力してみると、以下のようになりました。(左がキー、右が値)
debug   table: 00C9D770
vec4    true
coroutine       table: 00C98AA8
_G      table: 00C92760
package table: 00C98828
math    table: 00C9A2D0
my_class        userdata: 00C9C3B8
io      table: 00C98C38
os      table: 00C99880
bind_win        true
string  table: 00C99FB0
utf8    table: 00C9D6D0
table   table: 00C98BE8
キーを見ると、luaL_openlibsでロードされるLuaの標準ライブラリに混じってrequireでロードしたモジュール(ここではvec4, bind_win, my_classがそれです)の名前が見えます。対して値はそのモジュールをロードした時のreturn値が格納されています。(return値が無い場合はtrue)

また、この値は同じモジュールの二回目以降のrequireで返される値としても使われます。以下の例は、dofileが毎回math.random()を実行するのに対して、requireは初回呼び出し時の値をpackage.loadedにキャッシュする事を確かめるものです。


以上を踏まえたファイルシステム置き換え版のrequireは以下のようになります。