2014年02月18日

複数のウィンドウに対する描画

これまでキャラクターエディタやマップエディタなど、いろいろなツールを作ってきましたが、
たとえ複数のウィンドウがあった方が便利だな、と思った場合でも、無理矢理1ウィンドウにまとめていました。
なぜなら、複数のウィンドウに対して Direct3D で描画するのが非常に面倒に思えたからです。

複数のウィンドウがあるということは描画先のウィンドウハンドルが複数存在するということで、Direct3Dデバイスの作成時にウィンドウハンドルを渡している以上、ウィンドウハンドル1つにつき Direct3D デバイスが1つ必要なわけです。
デバイスを複数つくるだけならまだしも、テクスチャはデバイスごとに管理されているため、全く同じ画像を表示したい場合でも、デバイスごとに別々のテクスチャを作成しなければなりません。同じ絵を表示するウィンドウが2つあるなら、作成・管理するテクスチャの数も2倍になるということです。

これが非常臭そうに思えたので、Direct3D による描画が必要なツールを作る場合、ウィンドウが1つで済むよう、どうにかやりくりしていました。

.... とか思ってたら、なんか SwapChain つかえばできるよ的な話が聞こえてきました。
調べてみた所、まさに欲しかった機能です。
というわけで、いま描画先として二つのウィンドウハンドル hWnd1, hWnd2 がある場合、それぞれに描画する手順(DirectX9の場合)は:

- hWnd1 を指定した D3DPRESENT_PARAMETERS を用意し、普通に IDirect3D9::CreateDevice して IDirect3DDevice9 *dev を作成
- dev->GetSwapChain で hWnd1 に対応するスワップチェーン IDirect3DSwapChain9 *sc1 を取得
- hWnd2 を指定した D3DPRESENT_PARAMETERS を用意し、dev->CreateAdditionalSwapChain で hWnd2 に対応したスワップチェーン sc2 を作成
- hWnd1 に描画する時は、sc1->GetBackBuffer → dev->SetRenderTarget で描画先をセットして描画し、dev->Present で転送(BeginScene, EndScene忘れずに)
- hWnd2 に描画する時は、sc2->GetBackBuffer → dev->SetRenderTarget で描画先をセットして描画し、dev->Present で転送(BeginScene, EndScene忘れずに)

でいけるみたいですよ?(実は未確認)
posted by JUNOSOFT at 18:32| Comment(0) | プログラミング

2014年02月07日

ラスタライゼーションルール

ラインの端1ピクセルがが描画されないのって、もしかしてコレのせいなんですかね…?

http://msdn.microsoft.com/en-us/library/windows/desktop/bb147314#Point_and_Line_Rules
posted by JUNOSOFT at 21:10| Comment(0) | プログラミング

2014年01月26日

SurfacePro2

たまたま店頭で触った Surface 2 が予想以上にサクサク動いたので衝動買いしようかと思ったのですが、一応、メモリバカ食いと噂の Google Chrome の動作具合を見るために、その場でダウンロードしてインストールしてみました。
ところがインストーラを起動すると「この環境ではインストールできません」と…。
「この環境では」というのが気になってよくよく見てみたら、Surface 2 に入っているのは Windows8 RT だったんですね。
全然知りませんでした。

というわけで、ノーマル Windows8 が入っているという Surface Pro 2 を注文して既に2週間。
いまだ入荷せずの状態が続いています。品薄だとは聞いていたけど、ここまでだったとは…。
こういうのってキャンセルしたとたんに「入荷しました!」となるのは目に見えているので、気長に待ってみることにします。
posted by JUNOSOFT at 16:05| Comment(0) | 雑談

2014年01月25日

乗算済みアルファ

レンダーターゲットにアルファ付き画像を描画し、それをテクスチャとして別の画面に張り付けると、アルファ合成が二回行われるために透明度が下がってしまう…という問題があります。
これはそういうものだと思って諦め、やらないようにしていたのですが、単に乗算済みアルファ合成を使えばよかったんですね…。

言われてみれば確かにその通りで、簡単に解決する問題でした orz
posted by JUNOSOFT at 11:54| Comment(0) | プログラミング

2014年01月09日

ツリー構造をもったキャラクター

ツリー構造を持ったキャラクターがあるとき、普通に考えると

struct CHARA {
CHARA *ArrayOfChild;
};

のように、親が子を管理することになるのですが、場合によっては子が親を参照していたほうが楽になる場合があります。

struct CHARA {
CHARA *Parent;
};

というのは、親から子をたどっていくと、複数の子を持っている場合はそこで分岐が発生するのですが、子から親を辿って行くのなら分岐は発生しないからです。
キャラクターAがキャラクターBの子孫かどうかを調べるのは大変ですが(世代が離れるほど子孫が増えるので)、逆にキャラクターBがキャラクターAの先祖かどうかを調べるのは、非常に簡単だということです。
さらに、キャラクターごとに子キャラクターの配列を持たせる、なんて必要もなくなり、ただひとつ、親キャラクターへの参照を持つだけで済みます。
当然双方向ではなく子から親への片方向ツリー構造になるため、親から子になにかを伝搬させることは非常に無駄の多い処理になりますが…

例えばキャラクターAの子要素としてキャラクターB,C があり、さらにBの子要素としてDがあり、子要素の座標が親からの相対位置で定義されるような場合、トップダウン方式だとキャラクターAの座標を決め、再起処理などでB,C,Dの位置を決めていくことになります。しかし、もしキャラクター B, C, D が、たまにしか必要のない要素だとしたら?
毎回これらツリーのすべての要素の座標を求めるのは無駄です。そういうときはむしろボトムアップで座標を決めて行きます。Bの座標が必要になったら、その親であるAの座標を見る。まだ決定されていなかったら、そこでAの座標を決定する。Aの座標が決定したらBの座標も決まる。この時点でAとBの座標が分かります。つぎにCの座標が必要になったなら、その親であるAの座標を問い合わせます。Aの座標は既に求まっているので、計算せずにただAの座標を返すだけでOKです。結果的に、その時必要なオブジェクトの座標だけが計算され、その時使われなかったオブジェクトの座標は計算されないままになります。

posted by JUNOSOFT at 13:12| Comment(0) | プログラミング

2014年01月07日

お手軽輪郭線

2Dにしろ3Dにしろ、輪郭線を描画したい!! という需要がまれによくあるんですが、そういう時によく使っている方法です。
やり方は超簡単で、例えば1ピクセル幅の黒い輪郭線を書きたいのなら

位置を(-1, -1)だけずらし、黒で塗りつぶされた物体を描画
位置を(+1, -1)だけずらし、黒で塗りつぶされた物体を描画
位置を(-1, +1)だけずらし、黒で塗りつぶされた物体を描画
位置を(+1, +1)だけずらし、黒で塗りつぶされた物体を描画
本来の位置に、本来の色を使って普通に物体を描画

これだけです。これはポリゴンモデルの輪郭線にも、ドット絵の輪郭線にも使えます。
(ただし、1ピクセル幅の尖った部分を持つ画像などは、その先端部分の輪郭がうまく描画されません。完璧にキレイに描きたい場合は、上記に加えて(-1,0) (+1,0) (0,-1) (0,+1)だけずらした描画が必要になります)

Ace of Wands では、会話用の袋文字を表示するのにこの方法を使いました。

ちなみにこの方法だと、ほとんど重ねた状態で画像を5回描画するため、
物体が半透明だったり、輪郭線だけを半透明にしたい などの場合には対処できません。

まあ、本来ならピクセルシェーダーを活用するべきなのでしょうが、すごくお手軽かつキレイにできるので重宝しています
posted by JUNOSOFT at 12:02| Comment(0) | プログラミング

2013年12月27日

不正落ちしたときにエラー個所を特定する(シンプル説明)

この話題すでに一度書いたのですが(http://junosoft.sblo.jp/article/60096788.html
無駄に遠回りな書き方をした気がするので、もっとシンプルに書いてみます(本当は自分のメモ用なんですけどね)



■アドレスからエラー個所を特定できるように、map, cod ファイルを生成するよう設定しておきます。これがないとどうにもなりません
・「プロジェクト」→「プロパティ」を開き、「構成プロパティ」→「C/C++」→「出力ファイル」を展開
・「アセンブリの出力」を「アセンブリ コード、コンピューター語コード、ソース コード (/FAcs)」に設定する
・この設定でビルドしたものを、ユーザーに配布する



■いざエラーが発生したら、ユーザーに以下の操作をしてもらい、「障害オフセット」の値を調べます

・イベントビューアを開く
WinXPの場合
スタートメニューを開いて、「コントロール パネル」「パフォーマンスとメンテナンス」「管理ツール」をクリックし、「イベント ビューアー」を開く

Win7の場合
スタートメニューを開いて「コントロール パネル」「システムとセキュリティ」「管理ツール」をクリックし、「イベント ビューアー] を開く

Win8の場合
Windows + X キーを押してメニューを開き、「イベントビューア」をクリックする

・イベントビューアの左側にあるツリービューから、「Windowsログ」の「Application」を開く
・イベント一覧が表示されるので、「日付と時刻」を頼りに、エラーを探す
・ダブルクリックするとイベントの内容が表示されるので「障害が発生しているアプリケーション名」や「障害が発生しているモジュール名」が目的のものであることを確認する
・「障害オフセット」の値をメモする



■障害オフセットの値がわかったら、それに対応するソースコードを探します
・「障害オフセット」の値に、ベースオフセット 0x00400000 を足して「エラーアドレス」を求める
・ビルドしたときに生成された .map ファイルを開く
・関数名と関数アドレス、その関数を含む .obj ファイル名の一覧が並んでいるので、「関数アドレス(Rva+Base と書いてある部分)」を頼りに、「エラーアドレス」を含む「エラー発生関数」を探す
・「エラーアドレス」から「関数アドレス」を引いて、「エラー個所のオフセット値」を求める
・「エラー発生関数」を含む .obj に対応する .cod ファイルを開く
・ソースコードとそれに対応するアセンブリコードが載っているので、そこから「エラー発生関数」を探す
・「エラー個所のオフセット値」を頼りに、エラーが発生した場所を特定する



以上の操作で、エラーが発生した関数名、ソースファイル名、行番号などがわかります
posted by JUNOSOFT at 21:55| Comment(0) | プログラミング

2013年12月23日

FPS制御

FPS制御の話です

例えば毎秒 60 フレームで固定したい場合、おそらく真っ先に思い付くのは

while (...) {
Sleep(1000/60);
ゲーム処理();
}

です。ところが Sleep() の引数はミリ秒単位の整数なので、
本当は 1000÷60=16.666... ミリ秒待機しないといけないのが 16 ミリ秒の待機になり、
1ループあたり 0.666...ミリ秒だけタイミングが早くなることになります。
すると実際の FPS は 1000÷16 = 62.5 になり、毎秒 62フレーム+αになってしまいます。

ただしこれは、ゲーム処理の時間と、Sleep関数の誤差を考慮しない場合の話です。
実際にはゲーム処理にはそれなりの時間がかかるし、Sleep関数も、指定した時間だけきっちり待ってくれるとは限りません。
というわけで非常にシンプルですが、このやり方はボツです。




次に思いつくのは1秒の間での更新スケジュールを決めておき、その時間になるまで待機する、という方法です。
つまり

DWORD baseTime = timeGetTime();
DWORD frameCount = 0;
while (...) {
 DWORD nextTime = baseTime + 1000 * frameCount / 60; // 更新予定時刻
 DWORD currTime = timeGetTime(); // 現在時刻
 if (currTime < nextTime) {
  Sleep(nextTime - currTime); // 現在時刻が、更新予定時刻より前ならば待機
 }
 if (baseTime + 1000 <= currTime) {
  baseTime = currTime; // 1秒ごとに基準時刻を更新
  frameCounter = 0;
 } else {
  frameCount++;
 }
 ゲーム処理();
}

という方法です。1秒ごとに基準となる時間を設定し、その時間からの経過フレーム数によって
更新時刻を決定します。これは一見うまくいくのですが、
実はこの方法、重い処理の直後に早送り状態にあるという欠点があります。
処理時間が一定していれば問題ありませんが、通常処理の中に重い処理があると、
予定よりも遅れた分を取り戻そうとしてウェイトなしの早送り状態になります。
この早送りは、予定遅れが解消されるか、基準時刻が更新されるまで続きます。
つまり最大でも1秒間なので、この挙動には目をつむるというのもアリです。




その挙動が気になる方のために、解決方法を考えてみます。早送り状態なってしまうのは、
無理に予定に合わせようとしているのが原因なので、遅延した場合はいさぎよく予定をあきらめ、
無理のないスケジュールを組みなおせばよいわけです。
これには、上のプログラムを少し改造して以下のようにします

DWORD baseTime = timeGetTime();
DWORD frameCount = 0;
while (...) {
 DWORD nextTime = baseTime + 1000 * frameCount / 60; // 更新予定時刻
 DWORD currTime = timeGetTime(); // 現在時刻
 bool delayed = false;
 if (currTime < nextTime) {
  Sleep(nextTime - currTime); // 現在時刻が、更新予定時刻より前ならば待機
 } else {
  delayed = true; // 遅延発生
 }
 if (baseTime + 1000 <= currTime || delayed) {
  baseTime = currTime; // 1秒ごとに基準時刻を更新
  frameCounter = 0;
 } else {
  frameCount++;
 }
 ゲーム処理();
}

という方法です。前回のフレーム計測基準時刻から1秒経過するか、または予定よりも遅れている場合に基準時刻を取り直します。
ただ、これまでは1秒ごとの基準時間更新時に frameCounter の値を記録することで実行FPS値を取得できたのですが、
この方法だと遅延が発生した場合に frameCount が毎回リセットされてしまうため、実効FPSを取得することができません。

いろいろいじりまわした結果、最終的には以下のような形になりました

DWORD FPS = 60; // 目標FPS
DWORD currentFPS = 0; // 実際のFPS
DWORD baseTime = timeGetTime();
DWORD lastTime = timeGetTime();
DWORD frameCount = 0;
while (...) {
 DWORD time = timeGetTime();
 if (baseTime + 1000 <= time) { // 1秒ごとに基準時刻を更新
  currentFPS = frameCount * 1000 / (time - baseTime); // frameCount回更新したときの所要時間から、1秒あたりの更新回数を求める
  frameCount = 0;
  baseTime = time;
 }
 DWORD next1 = baseTime + 1000 * frameCount / FPS; // 予定された更新時刻
 DWORD next2 = lastTime + 1000 / FPS; // 早送りにならないようにした時の更新時刻
 DWORD next = max(next1, next2); // 遅いほうの更新時刻に合わせる
 lastTime = time;
 if (time < next) {
  Sleep(next - time);
 }
 frameCount++;
 ゲーム処理();
}



当たり前のようにやっているFPS制御ですが、意外と深いです

posted by JUNOSOFT at 20:54| Comment(0) | プログラミング

2013年12月16日

VC2013

Visual C++ 2013 Windows Desktop を使ってみたのですが、ようやくBOMなしUTF8に対応したみたいですね〜〜!!
おかげでソースをUTF8化することができそうです。日本語コメント付きのソースを XCode と共有しているときに頭の痛い問題でした。それにSJISだと文字化けするエディタとかあるので...
入力補完もかなり頭がよくなってきて、快適に入力できるようになりました。GJ!
posted by JUNOSOFT at 11:35| Comment(0) | プログラミング

2013年12月04日

Hgで、問題の二分検索を使う

現在作業中のリビジョンでバグが見つかったとき、どの段階でそのバグが混入したのか調べるために
二分検索を良く使っていました。

たとえばリビジョン 100 で正常動作していたものが、リビジョン 200 ではダメだった場合、
ちょうど中間であるリビジョン 150 を試してみます。これが正常に動いたら 150 から 200 のどこかで問題が発生したことになるので、
今度は 150 と 200 の中間である 175 で試します。ここで問題がでた場合、150 と 175 のさらに中間である 162 で試します。
これを繰り返していくと、少ない試行回数で問題が発生した最初のリビジョンを特定することができます。

ということを手動でやっていたのですが、いまさらながら TortoiseHG にそのための機能があることに気が付きました。
TortoiseHG Workbench のリポジトリメニューにある「問題の二分検索」です。なぜ今まで気が付かなかったのかが不思議なくらい、わかりやすいタイトルです。

ここで、「既知の正常なリビジョン」に正しく動いていた時のリビジョン番号を入力し、「取り込む」をクリック。
つぎに、「既知の問題リビジョン」にダメなリビジョン番号を入力し、「取り込む」をクリック。最新リビジョンを指定したければ -1 と入力すればOK。
すると自動的に中間リビジョンに更新してくれるので、その状態で試した結果に応じて
「このリビジョンは問題ありません」か「このリビジョンは問題あります」のどちらかをクリックしていきます。
同じことを繰り返していけば、やがて「みつかりました!」みたいなメッセージがでて、問題が発生した最初のリビジョンを教えてくれます。

いやあ、便利なものですね。
posted by JUNOSOFT at 15:46| Comment(0) | プログラミング