2013年03月25日

ステータスバーをのぞいた領域のサイズ

こんにちは。
ラーメンに胡椒をかけたら蓋が外れて全量投入という、ベタすぎる失敗をしてしまったプログラム担当です。

今日は Android の軽いネタなんですが、すこし混乱したの書いておきます。
最近作っているアプリはライブ壁紙としても、普通のアプリとしても起動できるようにしてあります。
当然コアな部分は完全に共通化しているので、どちらの状態で起動しても描画される画面は同じになるはずなのですが、ひとつだけ違うところがあったのです。

アプリ内において、とあるオブジェクトをアプリ画面の一番上 (つまり Android の画面全体で言うと、ステータスバーの真下) に描画しているのですが、通常のアプリとして起動したときはステータスバーの真下にぴったりくっつくよう、きちんと描画できているのに、ライブ壁紙として起動したときは、このオブジェクトがステータスバーの裏側に隠れてしまいます。

なぜ??と思ったのですが、よく考えたら当たり前でした。

ライブ壁紙として起動するとき、つまりホーム画面ではステータスバーは半透明になっており、壁紙の上に重なっています。
なのでライブ壁紙の描画領域も当然、ステータスバーが考慮されずに画面全体に及ぶわけですが、通常アプリとして起動した場合は、ステータスバーは完全に不透明な状態になっていて、アプリ側の画面とは一切重なりません。
だからどちらの状態で起動するかで、アプリが使用する画面の高さと領域が変化していたんですね。

というわけで、ライブ壁紙として起動されたときはステータスバーの高さだけ下にずらして描画し、通常アプリとして起動されたときはずらさずそのまま描画するようにしたところ、
どちらの状態でも意図したとおり、ステータスバーの真下に隙間なく描画することができました。
posted by JUNOSOFT at 21:42| Comment(0) | プログラミング

2013年03月15日

続々・謎のトーンカーブエフェクト

こんにちは。ピザにイースト菌を入れるのが面倒かつクリスピータイプのほうが好きなので、作るのはいつも超薄型パリッパリなプログラム担当です。
イースト菌を入れて放置する過程を全て省略するので、小麦粉をこね始めてから焼きあがるまで1時間もかからず、お手軽です。

前回公開した謎トーンカーブエフェクトツールですが、起動できないとのご報告を何通かいただきました。
調べてみたところ、ビデオカードがサポートしている OpenGL のバージョンが古い場合、起動に失敗するようです。
というか GLSL を使っている時点で OpenGL 2.0 以上が必須だということを忘れていました。失礼いたしました。

http://www.junosoft.net/tcfilter.zip

というわけでプログラムを修正し、OpenGL が GLSL に対応していればそれを使い、対応していない場合は代替手段を使って同じエフェクトをかけるようにしました。
古いバージョンの OpenGL でも動作する代わりに、速度・応答性が犠牲になっています。
なお、ビデオカードのドライバを更新すると、サポートされる OpenGL も新しいものになる場合があるので、最近ドライバ更新してないなーという方は試してみてください。
posted by JUNOSOFT at 15:25| Comment(0) | プログラミング

2013年03月13日

続・謎のトーンカーブエフェクト

車のカバーを外した状態で1日停めていたところ、見事に砂だらけになってしまい、初めて黄砂の威力を目の当たりにした気がするプログラム担当です。

今回は謎のトーンカーブエフェクト(http://junosoft.sblo.jp/article/63530387.html)の続きです。

しかしこのエフェクト、わざわざリアルタイムで適用しなくても、適当なレタッチソフトでトーンカーブをいじれば再現できるんじゃない?
とか思って Gimp でやってみたのですが、うまくいきませんでした。なんでだろう??

というわけでフィルターを適用して眺めるだけの簡単なプログラムを作ってみました。

http://www.junosoft.net/tcfilter.zip


起動したら、png または jpg をウィンドウにドラッグして画像をロード。
キーボードの 1, 2, 3 がそれぞれ R, G, B に対する ON/OFF の切り替えに対応。
カーソルキー左右で強度を変更できます。(パラメータはウィンドウのタイトルバーに出ます)
S キーを押すと exe と同じ場所にスナップショットを保存します。

ああ、ところで、スナップショットを保存させようとしてglReadPixels を使ったのですが、ファイルサイズによってはピクセルの取得ができず、強制終了してしまうことがありました。
どうしてかと思ったら、glReadPixels を使うときにも glPixelStorei での指定が必要だったんですね。

画像ロード時に指定していた glPixelStorei(GL_PACK_ALIGNMENT, 4) のまま設定を変えていなかったので、例えば1行あたり 123 バイト(横 41 ピクセル 24 ビットカラー画像とか)の画像を保存しようとすると、4 バイト単位の調整が入るために 124 バイトコピーしようとしてしまい、メモリエラーが発生していたようです。
気をつけましょう…。いや、みなさんはこんなミスしないと思いますが。
posted by JUNOSOFT at 09:50| Comment(0) | プログラミング

2013年03月10日

謎のトーンカーブエフェクト

こんにちは。自動車がトラブル続きのプログラム担当です。

GLSLを書いていたのですが、ピクセルシェーダーが出力したテクスチャの色が変になる、という不具合がありました。
元画像の黒い部分が白くなってしまうというものです。
ただ黒が白になるだけなら、まっさきに色反転を疑うのですが、そうではなく、色調をある程度保っています。


wall.png
たとえばこんな画像が

snapshot_r.png
こんな感じに!!


画像の明るさを変更しているだけかというと、そうでもなく、真っ黒な部分が白く変化するのに、元から色がついている部分はそんなに変化しません。
これ、フィルターとして使えそうだなーとも思ったのですが、なんにせよ原因を探って直さなければなりません。


そこに散らばっていた式をまとめてひとつの式にして単純化してみたところ、だいたい次のようになっていました。これが原因のようです。

gl_Fragment = TexelColor + pow(dot(normalize(vec3(BumpTexelColor.r, BumpTexelColor.g, 1.0)), Normal), Power)

バンプマッピング用に適当なRGB画像を用意して、そのRをX方向に、GをY方向に対応させて単位ベクトルを作成し、それをポリゴン法線に加えて法線の向きを乱し…とやろうとして、
式変形を間違えたようです。べき乗計算が入っているのは、べき乗の底 n が 0≦n≦1 であるという前提で n に対する明るさの変化が急激になるようにしつつ、
n^Power が 1.0 を超えないようにしようとした名残です。

とにかく、だいたいこの計算結果と似たような効果が得られるように注意しながら式を単純化していった結果、

gl_Fragment = TexelColor + pow(1.0 / (TexelColor.r^2 + TexelColor.g^2 + TexelColor.b^2 + 1.0 + MAGIC_NUMBER), Power); \n"

となりました。つまり、暗いピクセルはより明るくなり、明るいピクセルは明るいまま変化しないということです。
謎のマジックナンバー MAGIC_NUMBER は R, G, B すべてがゼロだった場合にべき乗の底が 1.0 になってしまい、
Power をいくら変化させても 1.0^Power は 1.0 のまま変化せず、したがって gl_Fragment が常に真っ白になったまま調整できない、という事故を避けるためで、
ようするに分子<分母 であることを保障するための適当に小さな数値です。

ここで

R = 1;
G = 0;
B = 0;
gl_Fragment = TexelColor + pow(1.0 / (R*TexelColor.r^2 + G*TexelColor.g^2 + B*TexelColor.b^2 + 1.0 + MAGIC_NUMBER), Power); \n"

とすると、RGBごとに特性が変化します。Rだけを1にすれば、赤い場所は赤いまま残しつつ、そのほかの黒っぽい場所は白っぽく変化します…
ちなみに加算ではなく乗算にすると

gl_Fragment = TexelColor * pow(1.0 / (R*TexelColor.r^2 + G*TexelColor.g^2 + B*TexelColor.b^2 + 1.0 + MAGIC_NUMBER), Power); \n"

R=1,G=0,B=0 とすれば赤を含む部分が真っ先に黒くなり、そのほかの色はあまり変化せずに残ります。


// vertex shader
#version 120
attribute vec4 Vertex;
attribute vec2 TexCoord;
uniform mat4 ModelView, Proj;
varying vec2 _TexCoord;
void main() {
 gl_Position = Proj * ModelView * Vertex;
 _TexCoord = TexCoord;
}

// fragment shader
#version 120
uniform float R, G, B, Power;
uniform sampler2D Sampler;
varying vec2 _TexCoord;
void main() {
 vec4 c = texture2D(Sampler, _TexCoord);
 gl_FragColor = c + pow(1.0 / (R*c.r*c.r + G*c.g*c.g + B*c.b*c.b + 1.0), Power);
}

snapshot_b.png

うまくやればドット絵みたいに簡単にパレット変更みたいなことが出来るかも……?(※つづく)
posted by JUNOSOFT at 03:21| Comment(0) | プログラミング

2013年03月06日

GLSLでハマル

こんにちは。
高校同窓会の通知がきたものの、出席名簿を見ても、全く覚えていない名前と、覚えているけど同じクラスだって知らなかった的な名前が半分を占めていて、行く気をなくしたプログラム担当です。

最近 GLSL と戦っています。HLSL がいかに楽だったのかを思い知らされました。
glGetUniformLocation で GLSL 側変数に値をセットしようと思ったのですが、
変数名を間違えていないのにエラーになってしまう場合があります。
これで散々悩んだ挙句にわかったのですが、その変数は最適化によって消されていました…。

uniform vec4 A, B;
...
gl_FragColor = A + B;

みたいにしている部分があったとして、テストのために変数 B の影響を一時的になくそうと思い、

gl_FragColor = A;

と書き換えたら当然 B は削除されるし、

gl_FragColor = A + B * 0.0;

でも削除されちゃうし、もちろん

if (0)
gl_FragColor = A + B;
else
gl_FragColor = A;

でも B が削除されてしまうので glGetUniformLocation(program, "B") はエラーになります。
じゃあそもそも glGetUniformLocation の戻り値なんて無視すればいいじゃん、エラー無視すればいいじゃん、となるのですが、
それだと本当に変数名を間違たときのエラーが検出できず、それはそれで困るんですよね。
前述の例で、GLSL 側の変更だけで変数Bの影響をなくしたいのであれば

gl_FragColor = A + B * 0.00001;

とかやっておけば、一応目的は果たせますが… なんとかならないものか。
posted by JUNOSOFT at 18:26| Comment(0) | プログラミング

2013年02月27日

Windows7(64bit)でGLUT

こんにちは、ビーフジャーキーを作ろうとして大失敗したプログラム担当です。

WindowsXPにGLUTを導入したときの感覚で 64Bit版Windows7にGLUTをサクサク入れようと思ったら、サクサク行かなかったのでメモしておきます。

http://www.opengl.org/resources/libraries/glut/
から Pre-compiled Win32 for Intel GLUT 3.7 DLLs for Windows 95 & NT をダウンロード。
ZIPを解凍してヘッダとライブラリを C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\include\GL と
C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\lib にコピー。
そして glut32.dll を C:\Windows\System32 ... ではなく C:\SysWOW64 にコピーします。
最初、いつもの感覚で System32 のほうに入れてしまい、アプリが起動できずに嵌ってました….
posted by JUNOSOFT at 10:08| Comment(0) | プログラミング

2013年02月20日

ADB-USBドライバ

こんにちは。
最近日本酒がとまらないプログラム担当です。


実験用のANDROID端末を手に入れたのですが、デバッグ用のドライバがないために adb から操作することができず、不便に思っていました。
そうしたら、なんとドライバを自力で用意(?)するという選択肢があることを知ったので、ここにメモしておきます。



以下、デバッグ用のUSBドライバを自力で用意する方法です。とても助かりました。


1. INFファイルを開く
\sdk\extras\google\usb_driver\android_winusb.inf をテキストエディタで開き、
32ビットPC なら [Google.NTx86] を、64ビットPCなら [Google.NTamd64] を見ます。
以下のような行がずらっと並んでいるはずです。それを確認したらとりあえず次のステップへ。

;Google Nexus One
%SingleAdbInterface% = USB_Install, USB\VID_18D1&PID_0D02
%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_0D02&MI_01
%SingleAdbInterface% = USB_Install, USB\VID_18D1&PID_4E11
%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_4E12&MI_01



2. 端末のハードウェアIDを調べる

端末をUSBでつないだ状態でデバイスマネージャを開き、「ほかのデバイス」にある、それっぽい端末のプロパティを開きます。
詳細タブを選択し、ドロップダウンリストから「ハードウェア ID」を選択。

例えば中華パッドPC の MOMO9 なら、以下のようなテキストが書いてあります。

USB\VID_18D1&PID_0003&REV_0230&MI_01
USB\VID_18D1&PID_0003&MI_01

この部分を丸写しして、先ほどテキストエディタで開いた android_winusb.inf ファイルの該当セクションの末尾に以下の行を追加します。
ちなみに ; で始まる部分は単なるコメントなので好きなように書いてOKです。

;MOMO9
%SingleAdbInterface% = USB_Install, USB\VID_18D1&PID_0003
%CompositeAdbInterface% = USB_Install, USB\VID_18D1&PID_0003&MI_01

まるまるコピペではないので注意してください。
「&REV」を含むほうのIDは、&REV よりも前の部分だけを CompositeAdbInterface に指定し、
「&REV」を含まないほうのIDは、CompositeAdbInterface にそのまま指定すればOKみたいです。



3. ドライバをインストールする

以上の手順で編集した android_winusb.inf をドライバとしてインストールしてください。


図入りでの詳しい手順は以下のサイトに載っています
http://novo7aurora.seesaa.net/category/12915501-1.html
posted by JUNOSOFT at 22:45| Comment(0) | プログラミング

2013年02月13日

luaをgccで使うときの修正

こんにちは。いよいよ明日はバ…


バチカンは大騒動なんでしょうね、今頃。
旅行でバチカンに行ったときの事を思い出したプログラム担当です。



Lua を含むソースコードを Android-NDK 向けにコンパイルしようとしたら ライブラリでエラーが発生してしまいました。
llex.c で未定義のメンバー decimal_point を参照している、と怒られました。
どうやら字句解析で変なエラーが起きている模様。

#if !defined(getlocaledecpoint)
#define getlocaledecpoint() (localeconv()->decimal_point[0])
#endif

見てみると、浮動小数の小数点を判別するマクロがおかしいようです。
フランス語やイタリア語だと小数点にはドット . ではなくカンマ , を使うので、
そういった書式に対処するためのものなんですかね?
とにかく、そういう配慮は一切無視して以下のように書き換えればOKでした。

#if !defined(getlocaledecpoint)
#define getlocaledecpoint() '.'
#endif
posted by JUNOSOFT at 16:04| Comment(0) | プログラミング

2013年01月30日

Jniで嵌った

こんにちは。そろそろバレンタインですね。
…いえ、なんでもありません。


ひさしぶりにハマりました。
Java から C へ、テクスチャー画像のピクセル配列を byte[] で渡そうとして次のように書きました。

EXPORT jint Java_package_class_method(JNIEnv* env, jobject obj, const void *pixels) {
 MyFunc(pixels);
 return 0;
}

どうも左上の数ピクセルの色がおかしいので調べてみたところ、受け取ったデータが 8 バイトずれていることがわかりました。
悩み、試し、ときには現実逃避ししていたのですが、あるときフッと気がつきました。
そもそも const void * で直接データを受けていることに問題があるんじゃないかと。
で、調べてみてらフツーにマニュアルに載っていたわけです。jbyteArray を使うんだと...

EXPORT jint Java_package_class_method(JNIEnv* env, jobject obj, jbyteArray pixels) {
 jbyte *buf = (jbyte *)env->GetPrimitiveArrayCritical(pixels, NULL);
 MyFunc(p);
 env->ReleasePrimitiveArrayCritical(pixels, buf, 0);
 return 0;
}

凡ミスだけど、なんかどっと疲れが…
posted by JUNOSOFT at 00:29| Comment(0) | プログラミング

2013年01月22日

ゲームプレイをAVIファイルに保存

おはようございます。
また雪が降ると聞いてワクテカなプログラム担当です。

今回は自分メモ用の小ネタです。

なんとかゲーム画面を綺麗に動画キャプチャーできないかと悩んだ挙句、スクリーンショットを毎フレーム連番ファイルに吐き出すという力技でいいんじゃね?ということになりました。

最初は PNG を律儀に毎秒60枚保存(当然、激しく処理落ちして「毎秒」にはなりませんが)していたのですが、そのうち、動画なら 20 FPS で十分じゃん、ということに気がつき、3フレームに1枚の割合で画像を保存するようにしました。

当然ながら大量の画像ファイルができるわけで、それならAVIにしてしまえということで、気軽に扱えるように簡単なAVI化クラスを書いてみました。
フォーマットは無圧縮 AVI のみですが、あとは好き勝手にエンコードすればよいわけで。

以下、コードです。
(文頭のインデントには全角スペースを使っているので、コピペした場合は Ctrl+H で一括変換してください)
 

#pragma comment(lib, "vfw32.lib")
#include
#include
#include // ceilf
#define AVI_BYTES_PER_PIXEL 3 // 24 bit color

class CAviWriter {
 BITMAPFILEHEADER mBmpFileHeader;
 BITMAPINFOHEADER mBmpInfoHeader;
 AVISTREAMINFO mAviStreamInfo;
 PAVIFILE mAviFile;
 PAVISTREAM mAviStream;
 int mImageIndex;
 int mWidth;
 int mHeight;
public:
 CAviWriter(int width, int height, int fps) {
  mWidth = width;
  mHeight = height;
  mAviFile = NULL;
  mAviStream = NULL;
  mImageIndex = 0;
  // BITMAPFILEHEADER
  const int w_align = (int)ceilf((mWidth * AVI_BYTES_PER_PIXEL) / 4.0f) * 4;
  ZeroMemory(&mBmpFileHeader, sizeof(BITMAPFILEHEADER));
  mBmpFileHeader.bfType = 0x4D42; // "BM"
  mBmpFileHeader.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);
  mBmpFileHeader.bfSize = mBmpFileHeader.bfOffBits + w_align * mHeight * AVI_BYTES_PER_PIXEL;
  // BITMAPINFOHEADER
  ZeroMemory(&mBmpInfoHeader, sizeof(BITMAPINFOHEADER));
  mBmpInfoHeader.biSize = sizeof(BITMAPINFOHEADER);
  mBmpInfoHeader.biWidth = mWidth;
  mBmpInfoHeader.biHeight = mHeight;
  mBmpInfoHeader.biPlanes = 1;
  mBmpInfoHeader.biBitCount = 8 * AVI_BYTES_PER_PIXEL;
  // AVISTREAMINFO
  ZeroMemory(&mAviStreamInfo, sizeof(AVISTREAMINFO));
  mAviStreamInfo.fccType = streamtypeVIDEO;
  mAviStreamInfo.fccHandler = comptypeDIB;
  mAviStreamInfo.dwScale = 1;
  mAviStreamInfo.dwRate = fps;
  mAviStreamInfo.dwLength = 10000; // 全部で何枚になるかわからないので、適当に大きな数を入れておく
  mAviStreamInfo.dwQuality = (DWORD)(-1);
  mAviStreamInfo.rcFrame.right = mWidth;
  mAviStreamInfo.rcFrame.bottom = mHeight;
  // Init API
  AVIFileInit();
 }

 ~CAviWriter() {
  End();
  AVIFileExit();
 }

 HRESULT Start(const char *aviPath) {
  mImageIndex = 0;
  HRESULT hr;
  hr = AVIFileOpen(&mAviFile, aviPath, OF_CREATE|OF_WRITE|OF_SHARE_DENY_NONE, NULL);
  if (FAILED(hr)) return hr; 
  hr = AVIFileCreateStream(mAviFile, &mAviStream, &mAviStreamInfo);
  if (FAILED(hr)) return hr; 
  hr = AVIStreamSetFormat(mAviStream, 0, &mBmpInfoHeader, sizeof(BITMAPINFOHEADER));
  if (FAILED(hr)) return hr;
  return S_OK;
 }
 
 void End() {
  if (mAviStream) AVIStreamRelease(mAviStream);
  if (mAviFile) AVIFileRelease(mAviFile);
  mAviStream = NULL;
  mAviFile = NULL;
 }

 HRESULT AddBitmapPixels(void *pixels, DWORD size) {
   HRESULT hr = AVIStreamWrite(mAviStream, mImageIndex, 1, pixels, size, AVIIF_KEYFRAME, NULL, NULL);
   mImageIndex++;
   return hr;
 }
 
 HRESULT AddBitmapFile(const char *bmpPath) {
  FILE *fp = fopen(bmpPath, "rb");
  if (fp) {
   DWORD size = mBmpFileHeader.bfSize - mBmpFileHeader.bfOffBits;
   void *pixels = (char *)malloc(size);
   fseek(fp, mBmpFileHeader.bfOffBits, SEEK_SET);
   fread(pixels, size, 1, fp);
   fclose(fp);
   HRESULT hr = AddBitmapPixels(pixels, size);
   free(pixels);
   return hr;
  }
  return E_FAIL;
 }
};
posted by JUNOSOFT at 05:38| Comment(0) | プログラミング

2013年01月11日

Visual C++ 利用者が Xcode で迷いそうなこと

こんにちは。最近 Android から iPhone へ乗り換えようかと考えているプログラム担当です。

最近 Xcode をちょくちょく触っていますが、まだ慣れていないせいで、VisualC++ で簡単にやっていたことでも戸惑ったりします。
以下、同じ境遇にある方のためのメモです。


■インクルードパスの指定

最初に Xcode を使ったときに微妙にハマったのがこれです。
インクルードパスを設定するときは、ターゲットを選んでから Build Settings --> Header Search Paths で入力します。
VisualC ではパス指定用のマクロを選ぶことができたのですが、Xcode にはそういった設定が見あたりません。
と思ったら、GUIで用意されていないだけで、同じようなコマンドを直接入力で使うことができました。
プロジェクトディレクトリは $(PROJECT_DIR) で指定できて、サブフォルダまで検索するときは ** と書いておきます。
例えばプロジェクトディレクトリ以下のすべてのディレクトリを検索対象とするなら、
$(PROJECT_DIR)/**
です。


$(SRCROOT) や $(BUILD_DIR) 等の Xcode で使用しているマクロの置換内容の一覧を調べる方法
http://d.hatena.ne.jp/shu223/20120804/1344155002

Build Setting Reference
https://developer.apple.com/library/mac/#documentation/DeveloperTools/Reference/XcodeBuildSettingRef/1-Build_Setting_Reference/build_setting_ref.html




■ターゲットOSの判別

MacOS 向けなのか、それとも iOS 向けにビルドしているのかをソースコード側で判断するには、TargetConditionals.h を使います。

#ifdef __APPLE__
#include "TargetConditionals.h"
#if TARGET_IPHONE
// iOS
#else
// MacOS
#endif
#endif

What #defines are set up by Xcode when compiling for iPhone
http://stackoverflow.com/questions/146986/what-defines-are-set-up-by-xcode-when-compiling-for-iphone



■外部データを読み込む

ユーザーが指定したファイルではなく、アプリで利用するデータファイルのロードについてです。
MSVCで作っていたとき、exeと同じ場所にデータファイルを置いておけば、単純に fopen("data.txt", "r"); のようにするだけでファイルをロードできたのですが、
Xcode だとデバッグ実行時のカレントパスがプロジェクトファイルとは全然別の場所(おそらくテスト用の一時ディレクトリ)になってしまい、
たとえ相対パスで指定してもデータを読むことができません。

簡単なのは使うデータをプロジェクトの一部として登録してしまうこと(VCでいうリソースファイル)です。
使いたいデータをXcodeにドロップしてターゲットに追加すると、以下のコードでこのファイルを取得することができます。
当然ながら、ObjectiveC 前提なので、C/C++側で利用したい場合は、あらかじめ検索パスをC/C++側に渡しておく必要がありそうです。

// ShiftJIS で書かれている mydata.txt の中身を得る
NSString *path = [[NSBundle mainBundle] pathForResource:@"mydata" ofType:@"txt"];
NSString *text = [NSString stringWithContentsOfFile:path encoding:NSShiftJISStringEncoding error:nil];


Xcodeはデザインもかっこいいですね!GUIデザインがよいアプリはモチベーション維持にも効果があると思っています。
posted by JUNOSOFT at 20:18| Comment(0) | プログラミング

2013年01月07日

iOSでC++

あけましておめでとうございます。年賀状が一通も来なかったプログラム担当です

(´;ω;`)ブワッ

そんなわけで正月ヒマだったので、Windows 向けの OpenGL アプリを iOS に移植するためのテストをしてみました。
以前、C++のプログラムを iOS に移植する話をした(http://junosoft.sblo.jp/article/57957388.html)とき、静的ライブラリを作成・利用する方法を紹介したのですが、実はアレ、かなり遠回り名ことをやっているということがわかりました。
実際にはもっとシンプルかつ楽にできたのです。
ObjecticeC から C++ のプログラムを呼びたいときは、ライブラリとかそういうややこしいことをする必要はいっさいなくて、要するに「ObjectiveC と C++ の両方を使っているソースファイルは、拡張子を hh または mm にする」だけでよかったのです。
例えば XXX.m に std::string message="HELLO WORLD" と書いてもビルドできませんが、ファイルをリネームして XXX.mm にすれば OK なのです。


自分用のメモもかねて、ここでは必要最低限の ObjectiveC + OpenGL なプログラムを作ってから、C++ コードを追加してみたいと思います。
まず Xcode 4.5 を起動して、iOS/OpenGL Game を新規作成します。
[File] -> [New] -> [Project] -> [OpenGL Game]
ここではプロジェクト名を MyGame にしたものとして話を進めます。


このまま実行すると立方体が表示されるデモが流れますが、今は邪魔なだけなので、OpenGLを使うために必要なコードだけを残して残りをバッサリ削除します。
すると、ViewController.m は次のようになります。

-----------------------------------------------------------------
#import "ViewController.h"
@interface ViewController () {}
@property (strong, nonatomic) EAGLContext *context;
@end
@implementation ViewController
- (void)dealloc {
[self tearDownGL];
if ([EAGLContext currentContext] == self.context) {
[EAGLContext setCurrentContext:nil];
}
[_context release];
[super dealloc];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.context = [[[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2] autorelease];
GLKView *view = (GLKView *)self.view;
view.context = self.context;
view.drawableDepthFormat = GLKViewDrawableDepthFormat24;
[EAGLContext setCurrentContext:self.context];
/* OpenGL初期設定 */
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
if ([self isViewLoaded] && ([[self view] window] == nil)) {
self.view = nil;
[self tearDownGL];
if ([EAGLContext currentContext] == self.context) {
[EAGLContext setCurrentContext:nil];
}
self.context = nil;
}
}
- (void)tearDownGL{
[EAGLContext setCurrentContext:self.context];
/* OpenGL終了処理 */
}
- (void)update{
/* 更新処理 */
}
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
glClearColor(0.65f, 0.65f, 0.65f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
/* 描画処理 */
}
@end
-----------------------------------------------------------------

さらにここから三角形を描画するだけのアプリを作ってみます。
まず、OpenGL ES2 ではなく OpenGL ES1 を使いたいので、上記プログラムで
self.context = [[[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2] autorelease];
となっている部分を
self.context = [[[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1] autorelease];
に変更します。そして glkView を

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
float v[] = {
0, -1, 1, 0, 0, 1,
};
glClearColor(0.65f, 0.65f, 0.65f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glColor4f(1, 1, 1, 1);
glVertexPointer(2, GL_FLOAT, 0, v);
glEnableClientState(GL_VERTEX_ARRAY);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 3);
glDisableClientState(GL_VERTEX_ARRAY);
}

にすればOKです。
さて、この状態では C コードとの混在しかできず、C++ コードを使うことはできません。
試しに #import と std::cout << "HELLO" << std::endl; を追加してみても、エラーになります。

そこで、ViewController.m を ViewController.mm にリネームしておきます。これだけで C++ コードを混在させることができます。
この状態で適当な C++ プログラム XXX.cpp と XXX.h をプロジェクトに追加し、#import "XXX.h" しておけば、既存のC++コードをいくらでも呼び出すことができます (^^
posted by JUNOSOFT at 22:19| Comment(0) | プログラミング

2012年12月27日

ライブ壁紙の強制終了

こんにちは。
静電気の季節になり、帰宅時にドアノブに触るとバチバチするのが嫌だったのですが、手ではなく、手に持った鍵でドアノブに触ればバチバチしても痛くない、ということに最近気がついたプログラム担当です。
手→(放電)→ドアノブ だと痛いけど、手に密着している金属→(放電)→ドアノブ だと大丈夫だということですね。


ところで最近、ANDROIDのライブ壁紙で原因不明なエラー報告が来ていました。

java.lang.RuntimeException: Failed to register input channel. Check logs for details.
at android.view.InputQueue.nativeRegisterInputChannel(Native Method)
at android.view.InputQueue.registerInputChannel(InputQueue.java:92)
at android.service.wallpaper.WallpaperService$Engine.updateSurface(WallpaperService.java:521)
at android.service.wallpaper.WallpaperService$IWallpaperEngineWrapper.executeMessage(WallpaperService.java:904)
...

ってやつです。InputQueue???という感じだったのですが、よくよく調べてみると、某ホームアプリを同時に使っていると起こる現象のようです。
わりといろんなBBSで質問をしている人がいたのですが、たいした回答はついていません。
唯一見つけた対処方法は「某ホームアプリがインストールされているかどうかを壁紙起動時にチェックして、インストールしてあれば警告メッセージを出しておく」というものでした。


というわけで以下のコードを使って 某ホームアプリを検出するように修正しておきました。

public static boolean isLauncherProInstalled(Context context) {
try {
PackageManager pm = context.getPackageManager();
PackageInfo info = pm.getPackageInfo("com.fede.launcher", 0);
return (info != null);
} catch (NameNotFoundException e) {
return false;
}
}

根本的な解決でないのがなんとも…。某ホームアプリのアップデートを待つしかないのでしょうか?
便利なんですけどねぇ、アレ…。
posted by JUNOSOFT at 18:34| Comment(0) | プログラミング

2012年12月20日

DLLの遅延ロード

こんにちは。プログラム担当です。


DirectX がインストールされていない環境でゲームを起動すると、起動に失敗して意味不明なエラーメッセージが出て、初心者が混乱する、ということがあります。

「d3dx9_**.dllが見つからなかったため。このアプリケーションを開始できませんでした。」

これではあまりに不親切なので、DLLがなくてもとりあえず起動し、きちんとしたエラーメッセージを出したい…というのが今日のテーマです。
普通にプログラムを作成しただけだと、DLL が見つからない場合には WinMain にすら入ってこないため、どうしようもありません。
というわけで、本当に必要になるまでは DLL をロードしないように「DLLの遅延ロード」を使います。

#include // DLLの遅延ロードに必要なヘッダ
#pragma comment(lib, "delayimp.lib") // DLLの遅延ロードに必要なライブラリ

static FARPROC WINAPI OnDLLEvent(unsigned notify, PDelayLoadInfo pdli) {
if (notify == dliFailLoadLib) {
// DLLのロードに失敗したときの処理
// DLLファイル名は pdli->szDll に入っている
}
return NULL;
}

int WINAPI WinMain(HINSTANCE hCurrInstance, HINSTANCE hPrevInstance, LPSTR szArgs, int nWinMode) {
 __pfnDliNotifyHook2 = OnDLLEvent; // DLLイベントが発生したときのコールバック関数を設定
 __pfnDliFailureHook2 = OnDLLEvent; // DLL失敗イベントが発生したときのコールバック関数を設定
 ...
 return 0;
}


さらに、遅延ロードするDLLをあらかじめ指定する必要があります。
[プロジェクト]→[プロパティ]→[構成プロパティ]→[リンカー]を開き、「DLLの遅延読み込み」にDLL名を書いておきます。
たとえば DirectX9 を使うプログラムならば

D3D9.DLL; DSOUND.DLL

というふうに指定します。ちなみに D3D9_24.DLL や D3D9_32.DLL など、D3D9_**.DLL という名前の DLL が必要になるのは DirectX9 2004 OCT の次のバージョンからで、
DirectX9 2004 OCT までは D3D9.DLL だけあればOKになっています。JUNOSOFT がリリースしているゲームはすべて DirectX9 2004 OCT を利用しています。

つくった EXE が具体的にどの DLL に依存しているかどうかは、ツールを使えばわかります。
自分は Dependency Walker というツールを使っています。

http://www.dependencywalker.com/

調べたい EXE を Dependency Walker にドロップすると、依存している DLL の一覧を表示してくれます。
posted by JUNOSOFT at 14:35| Comment(0) | プログラミング

2012年12月10日

スレッドセーフ

こんにちは。アニメグッズでそろそろ部屋が限界に近づいているプログラム担当です。

ところで今回はマルチスレッド処理で嵌った話です。小ネタです。どうもマルチスレッドは鬼門らしくて、毎回毎回何かしら嵌ります。
今回も例外でなく、いろいろ悩んだところがあったのですが、どうも根本的なところを間違えていたようです。
DirectXを使って、描画しつつ別スレッドでテクスチャをロードしてたのですが、たまに変な動きをするときがあるんです。
最初からスレッドセーフだと思っていたのですが、どうやらマルチスレッドで操作できるようにするためにはオプションを指定する必要があったみたいです。

IDirect3DDevice9::CreateDevice に渡すパラメータで D3DCREATE_MULTITHREADED を指定する必要があります。

しかし今さらCreateDeviceの引数で嵌るとは… orz
ちなみに OpenGL でも同じことをやろうとして失敗しましたが、こちらはそもそもマルチスレッドに対応していないようです(スレッドセーフでない)。
諦めましょう。
posted by JUNOSOFT at 22:33| Comment(0) | プログラミング

2012年12月06日

描画の効率化

こんにちは。親戚の子供x3の相手をして死にそうになったプログラム担当です。

今回は2Dゲームならでは(?)の描画効率化についてです。
2Dのゲームではアルファつきの画像を大量に扱うことになりますが、アルファ付きということは、たいていの場合、完全に透明なピクセルが画像の何割かを占めるわけで、これの描画コストが結構ばかにならないことがあります。

たとえばリング状のエフェクト画像なんかだと、テクスチャ四隅付近と中央付近には必ず余白ができますし、人物画像にしても、腕を前に伸ばして立っている絵なんかは、腕の上下に大きな余白ができます。
どんなに画像の大きさを切り詰めたとしても、画像が矩形でなければならない以上、こういった余白を削ることができません。

アルファ合成に設定している限り、これらの余白は絶対に描画されませんが(されては困る)、それでもテクスチャのピクセルとして含まれる以上は描画プロセスの処理対象になってしまいます。

たとえば実際に描画されるピクセルが全体の20%しかない画像を表示しようとした場合に、残りの80% のピクセルに対する操作はすべて無駄になるわけです。

そこで、「描画しない部分は、初めから描画対象にしない」あるいは「描画しない部分は、初めから画像に含めない」という方向で工夫することで、すごく効率が良くなります。



方法1 - 描画しない部分は、初めから描画対象にしない

元画像をいくつかの矩形エリアに分割し、それらをさらに「描画エリア」と「非描画エリア」に分けます。
Alpha > 0 であるピクセルを一つでも含んでいれば「描画エリア」、すべてのピクセルが Alpha == 0 なら「非描画エリア」です。
描画するときは、描画エリアだけを処理すればOKということになります。
具体的には、画像を16x16ほどの格子エリアに分割し、それぞれのエリアを1ピクセルずつ調べていきます。ひとつでも描画ピクセルが存在したら、そのエリアを描画エリアリストに追加し、これをすべてのエリアに対して実行します。
最終的には描画対象となるエリアだけを含んだリストが出来上がります。



方法2 - 描画しない部分は、初めから画像に含めない

方法1をデータの保存時にも適用するやり方で、「描画エリア」だけをリスト化するところまでは同じなのですが、ポイントはその状態でファイルに保存してしまう、ということです。
そうすると、余白部分をファイルに含めなくてもよくなるため、ファイルサイズが小さくなり、ロード時間も早くなります。さらにその状態でメモリにロードして描画するので、メモリ上に余白部分が展開されることもなく、メモリの節約、描画の高速化と、よいこと尽くめというわけです。

変換ツールを作って画像データを変換しておかなければならないので、それなりにめんどくさいですが…。
posted by JUNOSOFT at 00:19| Comment(0) | プログラミング

2012年11月30日

コンパイラには空気を読んでほしい

こんにちは。寒すぎて手が動かないプログラム担当です。


突然ですが、VC2010 で以下のように書くとエラーになりました。

class Klass {
public:
Klass() {}
};

void main() {
Klass a();
}


「Klass a() プロトタイプされている関数が呼び出されませんでした (変数の定義が意図されていますか?)」

唐突すぎて意味が分からなかったんですが、なんのことはない、言葉通り、プロトタイプと勘違いされてたんですね。
Klass を返す a という名前の引数なしの関数宣言と間違われていたと。コンストラクタの引数がない場合は、素直に

Klass a;

ってしときなさい、というお話でした。
posted by JUNOSOFT at 21:53| Comment(0) | プログラミング

2012年11月19日

GetViewport

こんにちは。
とある留学フェスタに、現地での体験談を語る役として参加したのですが、
8時間会場にいて4人しか話を聞きに来た人がおらず、暇で暇でしょうがなかったプログラム担当です。


今回は SetRenderTarget + GetViewport でハマった話です。
以下のようなプログラムを作った時に、なぜか変な挙動になっていました。

「GetViewport で現在のビューポートを取得して退避しておき、レンダーターゲットを変更して描画してから、SetViewport で元に戻す」

本当は上記文章の順番そのままに処理するのが正解だったのですが、
レンダーターゲットの設定とビューポートの設定は互いに独立した内容だと思い込んでいたため、

0) どこかで SetViewort を使ってビューポートを設定
1) レンダーターゲットを変更
2) GetViewport で現在のビューポートを退避
3) 好き勝手に描画
4) SetViewport でビューポートを元に戻す
5) レンダーターゲットを元に戻す


とやってしまい、おかしくなりました。
レンダーターゲットを変更してから GetViewport するか、
それとも GetViewport してからレンダーターゲットを変更するかは、
完全に好みの問題だと思っていたのです。
しかしレンダーターゲットのほうが小さい場合、(2) で得られるビューポートは、(0) の値よりも小さくなります。
気を付けましょう…
posted by JUNOSOFT at 18:42| Comment(0) | プログラミング

2012年11月16日

不正落ちしたアドレスからエラー箇所を特定する

こんにちは。最近陽気なサルデーニャ人と知り合ったのですが、彼がタラコスパゲッティ好きであることが判明し、驚いてしまったプログラム担当です。イタリアにも魚の卵を使ったソースがあって、それに似ているんだとか。


今回は、開発環境が無いPCでプログラムが強制終了したときに、エラー箇所を特定するためのメモです。
先日、とある方から、バトル・タンク・ソードW で不正落ちしたという報告を受けました。
親切なことに、そのメールにはワトソン博士のログが添付されていて、不正落ちしたときに実行された命令のアドレスなどが書いてありました。

ところがここで問題がありまして、そもそも、ソースコード側のどの部分がどのアドレスに対応するのかが分かりません。
そこで、関数名とアドレスの対応表(.map)と、ソースコードとアセンブラの対応表(.cod) を出力するように Visual C++ を設定します。

「プロジェクト」→「プロパティ」→「構成プロパティ」→「リンカー」→「デバッグ」→「マップファイルの作成」→「はい」
「プロジェクト」→「プロパティ」→「構成プロパティ」→「C/C++」→「出力ファイル」→「アセンブリの出力」→「アセンブリ コード、コンピューター語コード、ソース コード (/FAcs)」

落ちた場所の関数名のみ分かれば良いなら map だけで十分ですが、具体的に「このソースコードの何行目」まで特定したい場合には cod が必要です。


この設定でビルドすると、プロジェクトファイルと同じ場所に ***.map ができます。
また、出力フォルダ内にはソースファイルと同じ数だけの ***.cod が出来上がります。

プログラムを配布するときには、必ず、exe と一緒に生成された map と cod もバックアップしておきましょう。
自分はの手元には、プログラム公開時のソースコードがあったのですが、map と cod はありませんでした(そもそも、生成するように設定していなかった)。
そこで、当時のソースを再ビルドしてみたのですが、出来上がった exe は、配布したものと微妙に異なっていました。
バイナリが一致しません。
環境が Win7 から Win8 に移ったので、そのせいかもしれません。
とにかく、バイナリが一致しないということは、map の中身も cod の中身も、当時のそれとは異なっている可能性があるということです。
これでは役に立ちません。


そこで、新しく再ビルドした exe を件の方に送り、不正落ちエラーを再び出して、その時のワトソン博士のログを送っていただけるよう頼みました。
当然、exe と一緒に生成された map と cod は忘れずにバックアップしておきました。



その後、早速ログが届きました。ここからが原因追及の本番です。

まず、「フォールト」と書いてあるところを見つけます。そこが不正落ちした場所です。
ワトソン博士を使わなくても、不正落ちしたときに出るダイアログに表示してあるアドレスは、ここの部分を表しています。

---------------------------------------------------------------
ファンクション: d3d9!Direct3DShaderValidatorCreate9
4b70724e d2f7 shl bh,cl
4b707250 75dc jnz d3d9!Direct3DShaderValidatorCreate9+0x4787e (4b70722e)
4b707252 8b7de4 mov edi,[ebp-0x1c]
4b707255 8bce mov ecx,esi
4b707257 8945dc mov [ebp-0x24],eax
4b70725a 8b431c mov eax,[ebx+0x1c]
4b70725d 8b9008210000 mov edx,[eax+0x2108]
4b707263 8b7204 mov esi,[edx+0x4]
4b707266 8bc1 mov eax,ecx
4b707268 c1e902 shr ecx,0x2
フォールト ->4b70726b f3a5 rep movsd ds:036fd000=???????? es:03f70a98=41900000
4b70726d 8bc8 mov ecx,eax
4b70726f 83e103 and ecx,0x3
4b707272 f3a4 rep movsb
4b707274 8b75ec mov esi,[ebp-0x14]
4b707277 8b16 mov edx,[esi]
4b707279 8bce mov ecx,esi
4b70727b ff5208 call dword ptr [edx+0x8]
4b70727e 8b4de8 mov ecx,[ebp-0x18]
4b707281 8b06 mov eax,[esi]
4b707283 51 push ecx
---------------------------------------------------------------


今回はアドレス 4b70726b で落ちてました。
後述しますが、mapファイルによればベースアドレスは 0x400000 なので、このアドレスは大きすぎます…。
これは本当に自作プログラム内のアドレスなのか、と不安になります。
そこで関数呼び出しの履歴である「スタックバックトレース」を見てみます


---------------------------------------------------------------
*----> スタック バック トレース <----*
...
ChildEBP RetAddr Args to Child
0012f4d8 4b6f36f7 00000006 0000000c 00164560 d3d9!Direct3DShaderValidatorCreate9+0x478bb
0012f4ec 4b6e8add 00164560 00000006 0000000c d3d9!Direct3DShaderValidatorCreate9+0x33d47
0012f52c 004899df 00164560 00000006 0000000c d3d9!Direct3DShaderValidatorCreate9+0x2912d
0012f57c 00486f6b 444d8000 44340000 41000000 btsw+0x899df
0012f5a4 004c28ec 444b8000 44320000 447a0000 btsw+0x86f6b
0012f5d8 004c4f9e 444d8000 44340000 036f6100 btsw+0xc28ec
0012f5f8 004ca9cd 00c1dfd8 0047d68e 0004a85a btsw+0xc4f9e
0012f668 0047e69e 0004a85a 0004a85b 00000001 btsw+0xca9cd
0012f680 004cbc10 00000000 3ce7eedb 0047e72d btsw+0x7e69e
0012f694 004cbc36 00002710 00481df2 76af4e4f btsw+0xcbc10
0012f6b8 004cc1cb 00320037 00000000 7ffdf000 btsw+0xcbc36
0012ff30 004ebd1b 00400000 00000000 001423c6 btsw+0xcc1cb
0012ffc0 7c817077 00320037 00300033 7ffdf000 btsw+0xebd1b
0012fff0 00000000 004ebd6e 00000000 78746341 kernel32!RegisterWaitForInputIdle+0x49
---------------------------------------------------------------


一番上3行を見てみると、どうやら D3D9.DLL 内で不正落ちしたようです。
当然 D3D9.DLL のソースコードなどありませんから、その D3D9.DLL の関数を最後に呼び出した場所を見てみます。つまり4行目です。


---------------------------------------------------------------
0012f57c 00486f6b 444d8000 44340000 41000000 btsw+0x899df
---------------------------------------------------------------


D3D9.DLL の関数を呼び出した場所は 0x899df のようです。
map ファイルを見てみると、 Preferred load address is 00400000 とあります。
そこでベースアドレス 0x400000 に、関数の呼び出し元アドレス 0x899df を足します。

0x400000 + 0x899df = 0x4899df 

この 0x4899df を手掛かりにして、この場所を含む関数を探します。すると以下の行を発見しました。


---------------------------------------------------------------
0001:00088840 ?mogDXDrawCirclef@@YAXMMMH@Z 00489840 f mogDirect3D.obj
0001:000889f0 ?mogDXDrawRectf@@YAXMMMM@Z 004899f0 f mogDirect3D.obj
---------------------------------------------------------------


mogDXDrawCirclef は 00489840 から始まり、その次の関数 mogDXDrawRectf は 004899f0 から始まっています。
つまり 0x4899df は mogDXDrawCirclef 関数内のアドレスであることが判明しました。


つぎに、関数の開始位置 0x489840 から、D3D9.DLL を呼び出しているアドレス 0x899df のオフセットを求めます。

0x4899df - 0x489840 = 0x19F

どうやら、mogDXDrawCirclef の先頭から 0x19F バイト目で D3D9.DLL の関数を呼び出していて、そこで不正落ちしたようです。




mogDXDrawCirclef を含むソースファイルは mogDirect3D.cpp ですので、これに対応している mogDirect3D.cod を調べます。
どうにかして関数部分を見つけてください。
関数名だけで検索すると大量にヒットして大変ですが、引数まで含んだ関数宣言をそのままソースからコピペして検索するとすぐに見つかります。
そうして見つけたのが以下の行。


---------------------------------------------------------------
; 1963 : void mogDXDrawCirclef(float x, float y, float r, int partition) {
---------------------------------------------------------------


さらにそこから下にスクロールしていって、オフセットアドレス 0x19F にある命令を探します


---------------------------------------------------------------
; 1996 : }

0019f 5f pop edi
001a0 5e pop esi
001a1 5b pop ebx
001a2 8b e5 mov esp, ebp
001a4 5d pop ebp
001a5 c3 ret 0
---------------------------------------------------------------


1996行目の閉じかっこ } に対応しているということは、関数から抜ける瞬間あたりが怪しいですね。
スタックトレースを見ると D3D9.DLL の中で落ちているため、ソースコードを開いて mogDXDrawCirclef 関数の終了付近を探してみます。
すると、関数の一番最後に

g_D3DDev->DrawPrimitiveUP(D3DPT_TRIANGLEFAN, vertex_count, vertex_array, sizeof(VERTEX));

という行がありました。どうやらこの呼び出しで不正落ちした模様。


で、よくよく見てみると DrawPrimitiveUP の第二引数に vertex_count を渡しています。
本来ここにはプリミティブ数を渡さなければならず、D3DPT_TRIANGLEFAN なら、プリミティブ数=面の数=頂点数-2 になります。
つまり、vertex_count-2 としなければならなかったところを vertex_count にしていたため、実際よりも2枚多い三角形を描画しようとして、頂点配列の範囲外を参照してしまったということですね…。


ちなみにワトソン博士がいない場合、手掛かりになるのは不正落ちしたまさにその場所、今回の例でいうとアドレス 4b70726b です。
しかしこれはベースアドレス 400000 から離れすぎているし、map ファイル内を探しても対応する関数など見つかりません
そういう場合はダイナミックリンクしているライブラリを疑うべきみたいです。
今回はたまたまワトソン博士のスタックバックトレースがあったため、関数の呼び出し履歴から、自作プログラム内の特定の場所にたどり着くことができましたが、
そうでなかったら見つけるのは難しかったでしょうね…。


ところで、ワトソン博士、Win8 から消えてるんですが…
posted by JUNOSOFT at 21:59| Comment(0) | プログラミング

2012年11月03日

物理計算

こんにちは。
とある店でシーシャのストレートに挑戦したら、あまりにもガツンときてフラフラ酔ってしまったプログラム担当です。


エース・オブ・ワンドの最終ボスである○○○が持っているリボンと、ステージ2のボス(ブロックの壁に向かって攻撃するとビヨンってなるやつ)。

あれ、一応簡単な物理計算をやってるんです。

ブロック壁は、水面に波紋が広がるのと同じ考え方で、平面上に格子状に質点を並べ、それぞれをバネでつなげるという方法です。
各質点は、平面に対して垂直方向にのみ動くようにしています。壁が攻撃を受けた時に、攻撃が当たった場所に最も近い質点に対して適当な加速度を与えると、後はバネを伝って勝手に波紋のようなものが広がってくれます。
セル・オートマトンってやつですね。
質点の定位置からのズレに比例した力(距離が離れるほどもとに戻る力が大きくなる=バネ成分)と、質点の速度に比例した力(速く動かそうとするほど抵抗が大きくなる=ダンパー成分)の係数をうまく調整して、目的のアニメーションが得られるようにします。

一方、ボスが持っているリボンは縦に横に自由に動いてくれないと困るので、適当な数の質点をバネでつなげて鎖のようにしています。
これの場合、質点には常に下向きの力(重力)をかけておき、一番上の質点はボスに固定しておきます。
ボスが移動すると、バネでつながっている他の質点も連動して動くというわけです。

質点の動かし方は、まずそれぞれの質点にかかるすべての力を計算し(隣り合った質点とを結ぶバネの力と、重力など)、その力を質点の質量で割って加速度を出し、その加速度を、直前の時間における速度に加算して新しい速度を計算し、
その速度を、直前の位置に対して加算することで新しい位置を得ます。
ベルレ法ってやつです。


ポイントは、すべての質点の新しい速度と位置を計算し終わってから、一括して全ての質点の速度と座標を更新する、ということです。
更新しながら計算してはいけません。

点AとBがバネでつながっているとします。AとBは互いに影響しあっているため、Aの計算をするときにはAとBの両方の速度&座標を使うし、その次にBの計算をするときにも、同じくAとB両方の速度&座標が必要になります。
更新しながら計算していくと、Aの計算に使った時のAの速度&座標と、Bの計算に使った時のAの速度&座標が違ってしまうため、あまりよろしくありません。

あとはパラメータ調整の地獄ですよ〜
/(^o^)\
posted by JUNOSOFT at 21:37| Comment(0) | プログラミング