月別アーカイブ: 2007年9月

3D一般 ノーマルマップの互換性問題(2) UVに潜む罠

前回の続きです。
3D一般 ノーマルマップの互換性問題(1) ノーマルマップとは

●ノーマルマップに格納されるベクトルの座標系

ハイトマップからノーマルを生成する場合を考えてみます。
nVIDIA の Photoshop dds Plug-in にこの機能がついており
簡単に生成することができます。同様のツールは他にもあるし
いまどきのシェーダーならリアルタイムに生成するかもしれません。

まず基準となる座標系を決めておきます。例えば U-V を X-Y
平面とし、uv が左上原点の場合軸の向きを次の方向に想定します。

 U = X軸 + 方向
 V = Y軸 – 方向

右手座標系なら Z軸は手前(カメラ方向)が + です。
法線は -1.0~+1.0 の範囲の値を取るので、色情報として格納する
ために 0~1.0 に変換します。
8bit の場合はこれが 0~255 に相当します。

 R= X * 0.5 + 0.5
 G= Y * 0.5 + 0.5
 B= Z * 0.5 + 0.5

平面は常に手前の Z+ 方向を向いているので、法線の Z軸が負に
なることはなく常に 0.5~1.0 の範囲となります。

そのため画素値には必ず「青色」が一定量含まれており、作られた
ノーマルマップは一般的に青色に見えます。

完全に滑らかで凹凸の無い平面の場合は、どの法線も手前を
向いているので (0,0,1) になります。カラーにすると

 R= 0.5 = 128
 G= 0.5 = 128
 B= 1.0 = 255

の真っ青です。この X-Y 平面に配置した右手系ノーマルマップの
ことを別名「青マップ」と呼ぶことがあります。

一般的なタンジェントスペース用のノーマルマップのデータは
その多くが「青マップ」で、上記に近い座標系が使われる
ことが多いようです。

余談ですが、GeForce3 で私がはじめて作ったノーマルマップ用の
描画シェーダーは、何故か X-Z 平面で作っていて「緑マップ」
になっていました。当時ほかのツールなどの知識が皆無で、
何が標準なのか全く知らなかったので・・。

nVIDIA の Photoshop dds plug-in 等では、これらの軸を
入れ替えたり反転させたりと簡単に設定できるようです。
便利になりました。

TS (TangentSpace) 専用で用いる場合 Zが常に正なので
0~1.0 をそのままテクスチャに格納することもあります。

符号が不要なので、ATI の 3Dc や NVIDIA の Hi/Low フォーマット
など、法線圧縮を行う場合 X Y のみ格納します。
(Z は Z= sqrt(1-X*X-Y*Y) )

このように 2D で作成したノーマルマップのことを以下の説明で
「プレーンノーマルマップ」と呼ぶことにします。

●固定の座標系を持ったノーマルマップを任意の面に貼る

ノーマルマップは基準となる座標系に作られるので、必要に
応じて別の空間に変換する必要が生じます。

例えば X-Y 平面に作られた青マップを何も考えずに水面に貼ると、
法線は真横を向いているため真上の光源で暗くなってしまいます。
貼られた面のジオメトリに合わせて座標系を変換しなければ
なりません。

オブジェクトのローカル座標とノーマルマップが貼られた
UV を基準にした接空間(TangentSpace) の変換マトリクスを、
法線と同じように頂点単位で格納しておきます。回転なので
3×3 でよく、これはオブジェクト空間における接空間の 3つの
ベクトル軸に相当します。

 X = U = Tangent
 Y = V = Binormal
 Z = W = Normal

光源はワールド座標系に配置するので、ノーマルマップを用いて
光源計算を行う場合は下記の変換をたどります。
もちろん光源を逆変換してもかまいません。

 ノーマルマップの基準座標系
  ↓
 オブジェクトの座標系
  ↓
 ワールド座標系

TangentSpace 用のノーマルマップ (以下 TS Normalmap) は
データ作成時に決められた基準平面を持っており、自由に変換
できるので任意のポリゴン面に貼り付けることができます。
マップなど広い面積に適用する場合には、データを使いまわす
ことができるため TS Normalmap が必要です。

●オブジェクトスペースのノーマルマップとは

オブジェクトの表面の法線を、そのままテクスチャに書き込んだ
のが ObjectSpace (OS) のノーマルマップです。
(以下 OS Normalmap )
基準の面を持たず、オブジェクトに貼り付けた状態で正しい
ジオメトリとなります。

オブジェクトの形状に強く依存しているため、貼られた
テクスチャを他の形状のモデルや他の面に動かすことができません。

オブジェクトそのものの法線をそのまま表しているため、
すべての方向を向く可能性があります。X Y Z はどれも
-1.0 ~ +1.0 の値を取り、カラーで見るとさまざまな色が
混じった虹色です。

描画時は接空間との座標変換が不要で、TS Normalmap の描画と
比較すると頂点データサイズ減り GPU の負荷も低くなります。

データの流用ができないため、マップのように広い面積には
使えず、オブジェクトのような固定物に向いています。

キャラクタのような骨変形する物体に OS Normalmap を用いる
場合は、不可能ではないですがかなり注意が必要です。
基準となる形状を保存しなければならないので、ターゲットの
実機では使えても 3Dツール等では使えません。
以前のプロジェクトでは GPU が非力なので、少しでも処理を
稼ぐために OS も併用しました。
(この苦労話も機会があればいつか・・)

● OS Normalmap と TS Normalmap の相互変換

OS Normalmap と TS Normalmap は座標系が違うだけです。
それぞれ基準となる座標系さえわかれば相互変換は簡単にできます。
ハードウエアでもできます。

使い勝手や利便性を考えると、今の GPU なら TS Normalmap を
選ぶことになるでしょう。

● TS Normalmap は TangentSpace に依存する

TS Normalmap をどのようにポリゴン面に貼り付けるのか、
それを決定するのは頂点に埋め込まれた接空間マトリクス
(TangentSpace) です。

また TS Normalmap を作るツールも、UV と頂点座標から求めた
TangentSpace を元にデータを作成します。

TS Normalmap はオブジェクト空間に依存しない代わりに、
TangentSpace に強く依存してしまいます。

ノーマルマップを生成するツールの TangentSpace の算出と、
実際に描画するデータ内の TangentSpace が一致して
いなければ正しい結果になりません。

水面や垂直の壁など、わかりやすい平面の場合はあまり問題が
起きませんが、球状の物体など共有頂点でソフトエッジの
場合(または同じスムージンググループの場合)
TangentSpace はどうなるのでしょうか。

●本来の法線

ポリゴンで表現されたオブジェクトは、本来の形状を複数の
平面で近似したものです。平面故に曲面の表現は完全には
一致しません。見た目で誤差を埋めるために

・頂点の色を補間する
・光源計算の結果を補間する
・補間した結果を描き込んだ滑らかに見えるテクスチャを貼る

等の処理(ごまかし)が必要だったわけです。

頂点は離散的に本来の形状をサンプリングした座標値で、
同じように本来の形状の面の向きを、頂点位置でサンプリング
したのが頂点法線です。
共有される面法線の単なる平均ではなく、その点にあるべき
実際の面の傾きを表現しているといえます。

●共有頂点の TangentSpace

接空間も実形状の表面をなぞる向きに配置されていると考えられ
ます。法線の補間と同じように接空間も頂点間で補間します。

例えば球のノーマルマップを作成して 12分割程度のローポリゴン
モデルに適用することを考えます。

共有頂点のソフトエッジなら、接空間も補完によって法線同様
球状の表面に配置されます。補間された接空間と実モデルの法線
に差が無ければ、得られたノーマルマップはすべて (0,0,1) の
ベクトルを持つ真っ青でプレーンなマップになります。

●スムース補間問題

頂点法線が期待している形状と、接空間が作る表面形状にずれが
生じるとどうなるでしょうか。この状態でノーマルマップを作ると、
空間のずれをノーマルマップ側で吸収しようとします。
つまり

 ローポリ側の形状がノーマルマップに漏れ出している

わけです。
頂点法線と接空間を別計算で求め、お互いに整合性をとらない
場合に発生します。

接空間側で丸めるか、ノーマルマップ側で丸めるかの違いです。
ノーマルマップを作る上での考え方の違いといえるかもしれません。

ただし、このように漏れのある TangentSpace にはプレーン
ノーマルマップを貼ることができません。プレーンノーマルマップ
には、期待する形状の誤差を埋めてくれる情報が含まれていない
からです。

●UV ハードエッジ問題

UV は矩形のテクスチャをサンプリングする座標で 0~1 の範囲を
持った有限の値です。法線とは違い、シリンダなどに貼ると
必ず一周するつなぎ目ができます。

法線は共有し、かつソフトエッジだとしても (または同一の
スムージンググループに属しているとしても) 数値上 UV の値は
共有されていない非連続の状態ができます。

この条件において、法線ではつながっているにもかかわらず
接空間を UV と同じように非連続とみなしてしまうソフトが
あります。

つまり UV 値が非連続の場合、強制的に接空間だけ
ハードエッジになってしまうわけです。

例えば 3ds max 7 が生成するノーマルマップ用の接空間は、
UV の切れ目を常にハードエッジとみなしていました。

そのためシリンダや球面などの UV の接合面は、ノーマルマップ
側に曲面が描きこまれており、真っ青でプレーンな青マップに
なりません。部分的に UV の切れ目でグラデーションが発生して
しまいます。

3ds max 7 の中で閉じている分には同じ TangentSpace を使う
ため問題はありませんが、他のレンダラに持っていくと意図
した描画にならず切れ目が発生してしまうことがあります。

またこの場合もプレーンノーマルマップを貼る場合、切れ目の
部分だけおかしなことになります。
これも形状漏れの一種といえるでしょう。

●くるくるUV問題

単なるただの壁面に見えても、そこには作りこまれた職人の業が
あります。わずかなテクスチャを使い、リピートさせたり
回転させたり反転させたり、さまざまな貼り方を用いて
単調な繰り返しにならないように工夫されているのです。

技巧的なデザイナーは緻密に作りこまれた UV 値を持った
データを仕上げます。
ドット職人のドット技に似ているかもしれません。

ところがこのようなデータに対する耐性は多くのツールが
持っていません。

日本のゲーム会社でデザイナーが作ったデータの UV 値は、
反転は当たり前、回転も当たり前、テクスチャに対する UV の
オーバーラップもリピートも当たり前に使われていることが
あります。

複雑な UV を持ったデータでも、TangentSpace を求めて
ノーマルマップを貼ることができるかどうかが問題です。

レンダラによっては、UV を反転しただけでノーマルマップの
凸凹が逆になってしまうものもあるようです。

flip された UV で接空間が逆向きになり、共有時に相殺
されてベクトルが 0 に近づいてしまうこともあります。

ツールによって対応度が異なっており、例えば下記のような
問題があるようです。

・対策無しに補間してベクトルが 0 に近づいてしまう
   さらにその状態を無理やり正規化するので
   ねじれが発生するしそもそもつながっていない。

・ベクトルが一致しないため異なる頂点とみなして補間しない
   ハードエッジ化

上記問題が発生するため、もしかしたら開発チームによっては
UV 反転や回転を使ったデータの製作を禁止しているかもしれません。
伝え聞いた話では、デザイナーが手で修正してノーマルマップの
色を力技で書き換えているところもあったそうです。

UE3 や CryEngine など、有名どころではしっかり対応
しているのではないでしょうか。

私が くるくるUV問題 に遭遇したのは 5年ほど前で、実際に
開発していたコンシューマのプロジェクトでした。
(2004年に実際に発売されました)
その時はデザイナーの要望に応えつつ次のように対処しました。

・TangentSpace の符号が反転しているケースでも、
 共有頂点なら符号を残したまま同一直線状と見なして
 補間する

2年前に作った DirectX9 用エンジン (HYPERでんち のトップ絵等)
では、データ exporter が法線とは別に TangentSpace 専用の
スムージンググループを求めています。

●問題を踏まえて

このように、結構ツールによって生成される TS Normalmap
には差が生じています。

原因は特殊な UV 条件を想定していないことと、 ツール内で
完結している分には問題が表面化しないためだと考えられます。

次回はさらに、各種ツールの現状をいくつか取り上げてみたいと
思ってます。

3D一般 ノーマルマップの互換性問題(1) ノーマルマップとは

3D系ツールが吐き出すノーマルマップには、互換性が無い
ことが多いのです。

 ・スムース補間問題
 ・UVハードエッジ問題
 ・くるくるUV問題

DirectX8 の時代、ノーマルマップが出始めたころはまだツール
など存在せず、ほぼ自前でデータを作るしかありませんでした。
それゆえ互換性などの問題は皆無だったのですが、今になって
またデータ製作時にいろいろと不便が生じています。

というわけで、ノーマルマップ互換性問題についていろいろ
取り上げてみたいと思います。

●そもそもノーマルマップとは

リアルタイム系レンダリングでは、ノーマルマッピングはもう
ごく普通のありふれた手法となりました。
HW Shader が出始めたころに比べたらツール環境も雲泥の差で、
さまざまなツールが対応を謳っており、当たり前のように作成
できて当たり前のようにプレビューできます。

そのノーマルマップの使い方も、当初は表面に単純にでこぼこを
貼るだけの Bump Map 手法の一種、とそんな認識でしか
ありませんでした。

ところがハイポリの陰影をローポリで丸ごと再現してしまう、
目からうろこの応用方法が登場し、その威力がいくつかの最先端
ゲームで実証されると一気に広まりました。

ノーマルマップの現在の主な活用方法は 2種類あります。

 (A) 表面にでこぼこをつける BumpMap としての活用
 (B) 別の形状を再現し、見た目のポリゴン数を上げる目的

上は高周波成分に対する情報の寄与、下は低周波成分に対しての
ディテールアップといえるかもしれません。

その原理は単純なもので、あらかじめ法線情報をテクセル単位で
格納しておくだけです。マッピングによって、ポリゴンの各
ピクセルがそれぞれ異なる方向を向いていることに
「してしまう」のです。

このテクセル単位の法線を使って光源計算を行うと、単なる
ポリゴンの平面にまるで凸凹があるかのような陰影がつきます。

(A) も (B) も描画時は特に区別なく、シェーダーもプログラム
も同一のものが使えます。

これらは基本的にはデータの生成方法の違いで、ツールの進化
とデータ作成手段の開拓がもたらしたものです。

それゆえ、プログラマ的にはたいした大きなトピックが無く、
ほとんど CGデザイナーに依存してしまっている開発チームも
あるかもしれません。これもあとで説明する互換性問題が
なかなか表面化しない原因の1つではないかと考えられます。

●ノーマルマップの特徴と違い

ノーマルマップの利点は動的なライティングにあります。
テクスチャに書き込んだ陰影と違い、光源の位置と向き、
またカメラの位置によって陰やハイライトが動きます。

同時にノーマルマップを使うことは、結果として
 「ピクセル単位のライティング」
につながります。

見た目を考えるとこのピクセル単位のライティングは
大きな効果の1つだといえるかもしれません。

(1) 頂点単位ライティング(頂点ごとに法線)
(2) ピクセル単位ライティング(補間された法線でライティング)
(3) ピクセル単位に法線をマッピングする

(1) はグーローシェーディングで (2) はフォンシェーディング
に相当します。shader によって (2) と (3) はほぼ同時期に
実現可能となりました。

むしろ (2) のまじめな 法線の補間+正規化 よりも
ObjectSpace のノーマルマップを貼った方が実現は簡単です。

●ゲームの従来の手法

従来の頂点単位の ライティング+グーローシェーディングは、

 ポリゴンの割がはっきり見えてしまい、
 グラデーションもきれいならず、
 暗部などテクスチャの絵の色がつぶれてしまい、

視覚効果としてマイナスになってしまうことがありました。

マップなど固定物は光源の位置がほとんど変わらないので、
リアルタイムにライティングしないで、描き込んだテクスチャ
を用意した方がクオリティがあがって見えることがあります。

リアルタイムにライトを適用するのもエフェクト用の
瞬間的な点光源だけにして、あとはあらかじめデータに埋め
込んでおきます。明るさや影は頂点カラーに格納し、
テクスチャに余裕があれば重ねて影やライトマップにします。

ライティングしないのでスペキュラは出ませんが、代わりに
環境マップがよく用いられていました。

ノーマルマップでピクセル単位のライティングが可能に
なったこと、基本的に何らかのライティングしないと効果が
無いことから、データの作り方も大きく変化しています。

●ノーマルマップの威力

オブジェクトの形状を丸ごとノーマルマップに焼きこんでしまう
ことができます。

たとえばハイポリゴンで作ったノーマルマップを同じ形の
ローポリゴンモデルに貼り付けてしまいます。

ライティングを行うと、あたかもハイポリの形状がそこに
あるかのように錯覚してしまうわけです。

リアルタイムレンダリングでは頂点数の増加に比例して演算
負荷が上昇します。ノーマルマップの場合ピクセル面積が
一定なら、元のハイポリ形状は何百万ポリゴンだろうと一向に
構いません。

それででほとんど同等のクオリティを表現できるなら
非常に強力かつ便利な技術となるわけです。

ただし輪郭の形状は変わらないので、シルエットはローポリ
のままです。ローポリとハイポリの形状に極端な違いが
ある場合は視差による矛盾も大きくなってしまいます。
さすがに本物のディスプレースメントマップには適いません。

より改善された手法としてパララックスマップやレリーフ
マップがあります。これらもノーマルマップ自体は併用して
いますが、またいずれ機会があれば説明してみます。

●ライティングしないと意味が無い

ノーマルマップは法線情報なので、ライティングをしないと
意味がなくなってしまいます。光源がなければバンプ効果も
見えません。

光源の影響が弱い暗いシーンや固定のアンビエントライト
だけのシーンでは、単なるローポリゴンのマットな物体に
なりがちです。

従来の手法で作ったデータはテクスチャにも陰影が描き込ま
れているので、単純にノーマルマップを適用しただけでは
陰が重なってしまいます。

またノーマルマップを使うと単純に使用するテクスチャ量が
増えます。全体で使えるテクスチャメモリの制約を受けて、
解像度に制限が発生してしまうかもしれません。

ミップマップなどベクトル値に対するフィルタリングも
問題を含んでいます。たとえば一般のテクスチャツールで自動
生成されるミップマップはベクトルを徐々に打ち消し、平均化
してしまいます。
かといって縮小されたエリアで正規化をかけると、不連続な
ベクトルがハイライトのノイズを際立たせてしまい、逆効果に
なることもあります。

従来はあまりきちんとリアルタイムでライティングしない
ことが多かったので、設定がうまく決まらなければ手で描いた
方が楽だといわれてしまうかもしれません。

このように、使う場合にはそれなりのデメリットも発生します。
使えるテクスチャ解像度が下がり、かつきれいに効果が
見えないのなら使う意味がなくなってしまいます。

原理を把握し、それぞれの意味を理解た上でデータを作れるか
どうか。これがノーマルマップを活用する最大のポイント
かもしれません。

長くなってしまったので次回に続きます。

static タスクシステム

ほとんどのゲーム開発では、一般的にタスクシステムと呼ばれる
独自のフレームワークを使用しています。
あまりドキュメント化されたり表に出ることが無いものの、
プロジェクトを通じてプログラマ間に秘伝的に伝播していくので
実に多くのバリエーションがあります。

プロジェクトの数だけ存在するといっても良いかもしれません。
基本構造と目的はほぼ似通っており、セガタスクと呼ばれる
こともあります。

OS のタスク同様、処理ごとに CPU 時間を細かく分割していく
仕組みです。ゲーム開発で特徴的なのは、すべての処理を
フレーム単位で管理することです。
1フレームの時間は約 16.7~33.3msec と決まっているので、
この中でそのフレームに必要なタスクを、全部一巡しなければ
なりません。

タスクシステムの利点、またはその利用目的は、大きく分けて
2種類あると考えています

 ・タスク数の動的な増減への対処
 ・モジュール分割や作業分担といった開発時の利便性のため

ゲーム中必要な処理は能動的に変化します。例えばオブジェクト
数の変化 – 追加や削除など、動的な処理単位の変化を効率よく
管理することが求められます。

オブジェクトだけでなく、ゲームやシステム全体の動作自身も
タスク化することができます。各種モジュールやサービスなども、
ゲーム進行やモードの遷移にあわせて組み換えが可能となる
わけです。

オブジェクトのような細かい単位と、ゲームシーン等の大きな
処理単位では、規模も目的も異なります。

これらの処理単位を同じ枠組みを使って同一に扱うこともあれば、
グループ毎に別の管理機構を用いることもあります。親子関係、
階層構造を持たせて凝った構造にしているものもあります。
この辺の考え方や設計はそのプロジェクトの方針に依存します。

ゲームの開発規模も、今ではプログラマで10数人規模とかなり
大きくなっています。オブジェクトの増減といったプログラム
処理上の都合だけでなく、分業化におけるフレームワーク
としても非常に重要な役割を持っています。
ある意味人間も管理するわけです。

各タスクは独立しているため作業分担が比較的容易で、システム
がきちんとしていれば、お互いの干渉衝突を最小限にしつつ
開発を進められることになります。
(でも終盤はそうも言っていられません)

上で述べたように、1フレームの処理を切り分けながら処理を
組み立てていきます。各タスクは1フレーム内で順番依存を
持っており、実行順をある程度明確にしなければなりません。

さまざまな手法がありますが、例えば各タスクに割り当てた
プライオリティ値を元に、実行順番を決めるものもあります。

システム系モジュールや作業分担時のタスクは、動的な追加
削除はあっても、動的にプライオリティが変わることが
ほとんどありません。

そういう enum や define 値で順番を決められたタスクも、
動的なリストで追加時にソートされ、リストをたどって仮想関数
呼び出しが行われています。
これを static にできないか考えてみました。
(ちなみに筆者は script ベースのまた違う手法を使っています。)

いわゆる TypeList です。
今回はソートのために Binary Tree を作ってみました。

各 Task は任意の priority 値と呼び出し口 Func() だけ定義
すればよく、Func() が static なら基底クラスも不要です。
各 task は別ソースだとして

class DrawBegin_Task {
public:
    enum {
        priority= 3000,
    };
    static void Func()
    {
    }
};

class DrawEnd_Task {
public:
    enum {
        priority= 5000,
    };
    static void Func()
    {
    }
};

class Input_Task {
public:
    enum {
        priority= 1000,
    };
    static void Func()
    {
        //..
    }
};

こんな形で呼び出します。

template
struct task {
    enum {
        value= A::priority,
    };
    static void	Exec()
    {
        A::Func();
    }
};

typedef	TS::Array<
            task,
            task,
            task
        > TaskList;

void MainLoop
{
    TS::ExecTask::result>::Exec();
}

実際の実行順は TaskList 上の順番とは異なり、priority 番号
の小さい順に並び替えられます。

そのため一度 TaskList に登録してしまえば、各処理ごとに
priority を自由にいじって処理の挿入先を変更することができます。
これはコンパイル時に決定します。
画面をクリアするだけとか Flip するだけの単純な task は
inline 展開が可能です。(__forceinline が必要でした)

とはいえ、もともと 1フレームに数回程度、非常に実行頻度の低い
部分なので static 化したとしてもパフォーマンス的には全く
変わらないでしょう。

動的な変更を行うオブジェクト管理は、これらの下に設けることに
なります。

上の例では static な関数しか呼べませんが、各タスクの
インスタンスも管理したいならこんな感じになるでしょうか。
それぞれ _TaskBase を継承しておきます。
ついでに priority も外部からも指定できるようにしておきます。

class _TaskBase {
};

static _TaskBase*  _InstanceTable[ TS::Length::value ];

template
struct itask {
    enum {
        value= pri,
    };
    static void	Exec();
};

template
RegisterInstance( A* _instance )
{
    _InstanceTable[TS::Index >::value]= _instance;
}

template
void  itask::Exec()
{
    _TaskBase* _inst= _InstanceTable[TS::Index >::value];
    reinterpret_cast(_inst)->Func();
}

class Camera_Task : public _TaskBase {
public:
    enum {
        priority= 2000,
    };
    void Func()
    {
        //..
    }
};

使い方は TaskList に登録して、RegisterInstance() を
呼び出すだけです。

typedef	TS::Array<
            itask,
            itask,
            itask,
            itask
        >    TaskList;

void MainLoop
{
    RegisterInstance( new Camera_Task );

    TS::ExecTask::result>::Exec();
}

あとは MainLoop から priority 順に呼び出してくれます。
この場合も仮想関数は不要で、呼び出しは static の例と同じように
静的に行われます。
またインスタンスを持たないものは RegisterInstance() する必要が
無く、Func() も static のままで構いません。
(ここは本当は何らかのエラー判定が欲しい部分です。)

一応実際に試したのでソースをのせてみます。
(確認はVisualStudio2005のみ)

StaticTaskSystem.h

wheelhandle_ss01t.zip 実際に使用した例のアーカイブ

本当はもっといろいろ改良が必要だと思います。

WindowsMobile em1key 1.26 書き忘れた変更点

Ascii 遠藤さんの 「遠藤諭の東京カレー日記」
再び em1key 関連のお話が載りました。

Advanced W-ZERO3 [es] 親指(続編)
キーボードの限界小ささ(1)

今度はそれぞれキーボードに合わせてカスタマイズした、
実際の設定ファイルが公開されています。
em1key のカスタマイズ情報はそれほど多くないので、
実際に使っている方の実例として参考になります。

em1key v1.26 で Advanced W-ZERO3[es] に(一応)対応しましたが
内蔵キーボードとの切り分け共存が 100% できているわけではなく、
Fn を押した記号と親指シフトの定義が干渉することがあるようです。

とりあえず oyayubiwm の方に機能の有効・無効をいつでも
切り替えられる機能がついているので、こちらでの対処を
お願いします。具体的な操作を質問いただいたので、
下記ページにも追記しておきました。
oyayubiwm

em1key v1.26 では他にも変更点がありました。
ドキュメントへの追記と script マニュアルの更新をすっかり
忘れていまして、あわてて更新をしておきました。

script 命令の ifpc, ifce, ifsw, ifnsw ~ endif が
ネスト対応になっています。(いまのところ 32段まで)

ifpc
ifce
ifsw 
ifnsw 
endif
define  

これらの命令は C 言語でのプリプロセッサに相当します。
script ファイルを em1key が読み込んだ時点で処理されるので、
if ~ 等で除外されたブロックはバイトコード化されず、
メモリにも常駐せず、負担にならないようになっています。
動的な分岐は IF_~ 系の命令です。

em1key スクリプトマニュアル v1.26.0
em1key カスタマイズ情報

Direct3D 10 Shader4.0 ID3D10Include の活用

引き続き HLSL/Effect のコンパイル周りの話です。
core API 側の Shader Compiler で #include に対応するには
ID3D10Include の準備が必要です。
これはもともと Direct3D9 の D3DX にあったもので、
機能も使い方もいっしょのようです。

ただ、D3DX10 の Shader Compiler API を使う分には内部で
勝手に include 処理をやってくれますし、下記エントリで書いた
ように D3DX 側 API を使った方が良いので、普通に使う分には
あまり気にする必要は無いかもしれません。
>・Direct3D 10 Shader4.0 APIによってコンパイラが違う

一応試してみました。

class fxInclude : public ID3D10Include {
public:
    fxInclude()
    {
    }
    ~fxInclude()
    {
    }
    virtual HRESULT __stdcall  Open(
                D3D10_INCLUDE_TYPE inctype,
                LPCSTR filename,
                LPCVOID parentdata,
                LPCVOID* ppdata,
                UINT* pbyte )
    {
        UINT   bsize= __FileSize( filename );
        void*  ptr= __Alloc( bsize );
        if( ptr ){
            if( __Load( ptr, filename, bsize ) ){
                *ppdata= ptr;
                *pbyte= bsize;
                return	S_OK;
            }
            __Free( ptr );
        }
        return	E_FAIL;
    }
    virtual HRESULT __stdcall  Close( LPCVOID ppdata )
    {
        __Free( const_cast( ppdata ) );
        return  S_OK;
    }
};

使っているところ。

ID3D10Blob* blobEffect= NULL;
ID3D10Blob* blobError= NULL;
fxInclude  incFunc;
D3D10CompileEffectFromMemory(
    memory,
    size,
    fxname,
    cpp_defines,// def
    &incFunc,	// inc
    0,		// HLSLflag
    0,		// FXflag
    &blobEffect,
    &blobError
);

#include の度にファイル名を持って callback されるので、
必要なメモリ領域を作ってあげるだけです。
不要になったら Close() を呼び出してくれます。

ID3D10Include を自前で用意する目的として考えられるのは、
include path 検索への対応でしょうか。

もしくは File にこだわらず、特定の include キーワードに
反応して別のコマンドを挿入したり pragma のように動作を
変えることも出来そうです。
自分で Effect のテキストを読み進める処理が不要なので、
意外に使えるかもしれません。

というわけで

 #include “output_disassemble”
 #include “output_preprocess”

と記述したシェーダーは、デバッグテスト用に disassemble や
preprocess 結果をファイルに書き出すようにしてみました。

テスト&デバッグ中は毎回出力して欲しいけど、完成した
シェーダーの場合は余計なファイルを作られても困ります。

今までは debug build の場合無条件に両方書き出していたので、
不要なファイルが多数生成されていました。これで、完成した
シェーダーは余計なことをしなくて済むようになります。
あまりきれいな方法じゃないけどちょっと便利になりました。