Archives

September 2007 の記事

DX10/D3D10 では Effect(fx)/HLSL のプリプロセッサが拡張されている
ことを下記のエントリで書きました。
Direct3D 10 HLSL の関数型マクロ定義

その後もう少しだけ調べてみましたが、残念ながらまだ C/C++ と
完全互換というわけにはいかないようです。
もしかしたら BOOST_PP でも動くんじゃないか、と思って試したら
甘かったです。


●引数無しの関数型マクロが定義できない

#define NAME_A() 0x40f

上記のような引数無しの関数型マクロ名がエラーになります。
ダミーでも NAME_A(_r) のように何か引数を与えないとだめでした。


●意味のない # 行が無視されない

例えば単独で出てくる行頭の '#' のみの行がエラーになります。


●#if 等で bit 演算できない

#if BITFLAG & 4

こんな形の bit 判定はエラーになります。| や ^ 等もだめ。
一般的な四則演算や論理演算はもちろん大丈夫です。


Effect(fx)/HLSL 単体でここまで凝った使い方をすることはあまり
無いかもしれません。
ただ、定数シンボルなどを共通化するために C++ ソースとヘッダを
共有する可能性はあるので、この辺の互換性には注意しておきたい
ところです。


内蔵 Preprocessor は動的コンパイルでは必要になります。
でも、もし完全に事前コンパイルしてもいいのならば、普通に C++ の
プリプロセッサを通した方が良いかもしれません。
従来 DirectX8(vsa/psa)~DirectX9 の時はいろいろ機能的に
物足りなかったので、例えば下記のような感じで Makefile を
作っていました。

# Makefile
# nmake 用

PROFILE	= fx_4_0
FXINCLUDE = -I.

.fx.fxo:
	cl /E $< $(FXINCLUDE) > $*.tmp
	fxc /T$(PROFILE) /Fo$@ $*.tmp

.SUFFIXES:	.fx .fxo

sysdef.fx というファイルが存在する状態で sysdef.fxo を作りたければ
コマンドラインから

nmake sysdef.fxo

とキータイプするだけです。Makefile はカレントに必要です。
nmake.exe や cl.exe、fxc.exe 等にパスを通しておく必要があります。

VisualStudio は Common~\IDE と VC\bin の2箇所にパスが通って
いれば OK です。例えば 32bit(x86) + VS2005 でパスを通しておく
フォルダは下記の通り。

%DXSDK_DIR%Utilities\Bin\x86
C:\Program Files\Microsoft Visual Studio 8\Common7\IDE
C:\Program Files\Microsoft Visual Studio 8\VC\bin

VisualStudio にこだわる必要は無く、gcc など他の C/C++
プリプロセッサやコマンドでも全く同じように使えます。
Makefile の書式は make コマンドによって若干異なります。
ちょっと混用して gmake + cl だとこんな感じでしょうか。

# gmake の場合
PROFILE	= fx_4_0
FXINCLUDE = -I.

%.fxo: %.fx
	cl /E $< $(FXINCLUDE) > $*.tmp
	fxc /T$(PROFILE) /Fo$@ $*.tmp



SDK マニュアルを見ていて HLSL Grammar の Operators に
'##' や '#@' という演算子を発見しました。
これは本来 #define マクロの引数に適用される演算子です。

これまで Effect fx/HLSL のプリプロセッサは非常に制限された
ものだと思っていました。例えば

・#define も基本的に定数や #if 用。
・引数を伴う関数型のマクロ定義は出来ない。

などなど。
もしやと思って実際に試してみたら、なんと関数型の引数を持ったマクロも
普通に定義して使うことが出来ました。

例えば上記演算子を使ってみるとこんな感じです。

#define	VARNAME(_n,_m)     _n##_m
#define	SEMANTIC_TEX(_id)  TEXCOORD##_id

float VARNAME(light,ambient)= 1.0f;

void VS_Main( uint vid : SV_VertexID,
			out float4 opos : SV_POSITION,
			out float2 ouv : SEMANTIC_TEX(0) )
{
  :

この場合 VARNAME(light,ambient) は「lightambient」と
連結された変数名になるし、SEMANTIC_TEX(0) は TEXCOORD0 に
置き換わります。

#@ は文字定数 '~' への変換に相当します。例として次のように
記述してみます。

#define LCHAR(_n)	#@_n
int id= LCHAR(Z);

LCHAR(Z) は 'Z' に置き換わりました。つまり

int id= 'Z';

と等価です。

引数の文字列定数化を行う、単一の '#' 演算子はありませんでした。

使えないと思っていたのでかなり驚きました。思い込んでいただけで
しょうか。
SDK Sample fx から "define" を検索しても、やはり引数を持った
関数型の define は全く使われていないようです。

試しに DirectX SDK 2006 October の fxc.exe (DX9向け) で実行したら
エラーになりました。
同じ 2006 October の fxc10.exe ではコンパイルが通ります。
D3D10 で HLSL 用プリプロセッサの機能拡張が行われたことは間違い
なさそうです。

Preprocessor は HLSL コンパイラ DLL 呼び出しとは別なので、/LD
オプションは使えませんでした。
D3D10 core API 側の Preprocessor が呼ばれると拡張タイプになり、
D3DX9 側の Preprocessor だと従来どおりの簡易型になるのではない
かと考えられます。


結論
・Direct3D10 の HLSL/Effect(fx) では、従来使えなかった関数型の
 マクロ定義が使える
・さらにマクロ引数の演算子 ## と #@ も使える


もう十分きれいになったから、これ以上いったいハードに何が
必要なんだ、とゲーム開発の現場でもよく聞く話です。
進化する必要が本当にあるのかと。
画面の見た目の変化はある一定以上まではわかりやすいですが、
それを超えると素人目にはあんまり区別がつかないかもしれません。

従来そのままのデータや作り方を続けている限り、ハード機能や
世代が変わっても映像的にはさほど変化が無いというのが1つ。

もう1つは、前処理で作っておくフェイク手法がかなり発達しており、
作業工程も確立していること。プリレンダリングしたり前処理で焼き付けたり、
ムービーを併用していたり。変化しない限り、動かない限りは
遜色ないクオリティを出すことができます。

本来のインタラクティブ性を完全に再現するにはシミュレーションが
必要ですが、これはまだまだパワーが足りません。
物理エンジンにしろアニメーションにしろ、今ようやく本格的な
活用が始まったばかりです。

shader が登場し、ソフトウエア技術者の工夫と努力が可能になって、
GPU のパワーが上がった今行われているのは、フェイクを徐々に
本来のものに置き換える動きだとといえるかもしれません。

見た目ではほとんど変わらなくても、ライティングがリアルタイムに
なって影も動的生成になって、やっと自由に動かせるようになって
きました。
でもまだまだパワーが足りず十分ではありません。
もっともっと欲しいのです。メモリもパワーも。

●流体力学による煙、炎、水

というわけで・・このセッションでは、煙等の流体を GPU 上で
シミュレートする手法について丁寧かつ実践的に詳しく解説が
行われました。シミュレートだけでなく動的なインタラクションと
レンダリングも含まれており、説得力のある煙となっています。
表現上、そしてパフォーマンス的に問題となる点にもきちんと
踏み込んでおり、良く考えられたわかりやすいものでした。
ただしシミュレートはまだ空間上非常に限定された領域に限られ
ています。

注目したのは、ボリュームテクスチャへのレンダリングが実用的に
なる(なっている)という点。これはかなり応用できそうですね。

ボリュームの多用はメモリ容量が心配ですが、毎年発売される
GPU のスペックを見る限り、間違いなく時間が解決するでしょう。


●いつまでもプレイできる (今日と明日のためのゲームアート)

スペックの予測を立てるためのデータとその理由が流暢に
語られます。流暢過ぎるために若干流れの趣旨がつかみづらい
ことと、少々冗長に感じてしまう部分があります。
というのも、対象が純粋な技術者ではなく、アーティストや
プロデューサーを含めた幅広いものだったからのようです。

昔作られたゲームのグラフィックスを、時代に合わせて改善する
手間のかからない いくつかのテクニックが紹介されます。
とはいっても具体的な手法そのものにフォーカスした内容では
なく、最新 GPU によってパワーがあまってもこんな感じで
さまざまな「工夫が可能なのだ」ということ。
あきらめるな、ということです。


●高度なスキンレンダリング

GeForce8800 のデモのひとつ、リアルな皮膚表現の解説でした。
GPU Gems3 の表紙でもおなじみのあれです。
プログラマはもちろん、むしろCGデザイナー(アーティスト)の方
が見ても大変参考になる内容だったと思います。
効果の比較もわかりやすく、なぜそのような処理が必要なのか、
なぜこのような構造になっているのか、さまざまな工夫とともに
詳しく説明されました。

テクスチャスペースにおけるライティングやエフェクトは非常に
魅力的なものです。メモリさえあれば。
メモリ容量の増加によって、できることや可能性が増えている
ことが良くわかります。


●みんなのためのシェーダつくり (一般向けのシェーダ)

FX Composer2 の紹介です。
さまざまなフィードバックにより、さまざまな用途、さまざまな
目的に利用できる完成度の高いツールに進化しています。
基本的には拡張性とカスタマイズ性を持った、開発に必要な
機能を統合するための入れ物的なポジションです。
豊富なシェーダーライブラリから、目玉のシェーダーデバッガまで
ありとあらゆる。実演も行われました。

これまでこの手のツールに消極的だったのは、機能や使い勝手
以前のところに理由がありました。例えば NVIDIA 以外の GPU で
の動作確認。使えない機能があるのではないかという危惧です。
本格的にコンテンツパイプラインに組み込むならば検証して
おきたいところです。
D3D10 以降では基本的に機能差が無い前提なので、もしかしたら
特に問題ないのかもしれません。



Direct3D や Shader 関連は HYPERでんち のページでまとめています。
blog に書いた内容もある程度分類して参照できるようにしていますので、
HYPERでんち の方もぜひこの blog (ホイール欲しい ハンドル欲しい) と
合わせてご覧ください。

HYPERでんち


前回迷路を作成するシェーダーを作りました。
Direct3D ShaderModel4.0 Shaderで迷路作成

でも作成した迷路が本当に端から端までつながっているのか、
壁がどっかでつながっていたりしないか確認するのが大変なので、
今度は自分で探索させてみました。

ss04 maze 128x128

まず迷路を作り、その後色のついたピクセルが勝手に歩き回ります。

キャプチャだけだと良くわからないので、これも実際に走らせて
ぜひ動いているところをご覧ください。
動いているところだったら、何をやっているのか一目瞭然だと思います。

今回は動きがわかりやすいように 128x128 の初期データが入ってます。
ss03(前回)の initdata.png を使うと 512x512 の迷路になります。
拡大しないとわからないですが暇な時にじっくり見るのにお勧めです。


基本的にすべての探索点は同じアルゴリズムで動いています。
どれも右側の壁伝いに歩いてすべての通路を歩きつくそうとします。
ただしお互いにも衝突するので、狭い路地から出られなくなったり
ショートカットして違う分岐に進んだりと結構ランダムに散ら
ばってくれるようです。

壁伝いに動くため、壁を見失うとその場でくるくる回ってしまい
ます。誰かが助けに来てくれるまでそのままです。

探索点はランダムに左上 (1,1) の点から生み出されます。


シェーダーは下記の 2つです。
 ・maze.fx (迷路生成、前回のものと互換性あり)
 ・walk.fx (迷路探索)

cpp 側のソースを見るとわかりますが、シェーダーを適用して
Draw( 4, 0 ) で四角形を1つ描いているだけになっています。

処理は全部 PixelShader です。迷路のデータを元画像として
レンダリングすると次のシーンが出来上がるわけです。
使っている画像フォーマットも通常の 32bit (R8G8B8A8_UNORM) です。

walk.fx での内部的なピクセルの意味は次の通り。

R= 0.00 路地
   0.25 衝突判定用予約点
   0.50 探索点
   1.00 壁
G= 向き (探索点と予約点のみ) 0.25単位で 上下左右
B= 向き確定フラグ (探索点のみ)
A= 探索点の色

フレームバッファに転送する際に内部ワーク状態を消すようにした
ので、ちらつきも無く見やすくなっています。その代わり判定
している様子など内部状態は見えなくなりました。

walk.fx で使っている乱数は1つだけ。探索点の生成と探索点自身
の色決定です。移動時の判定順は次の通り。

(1) 未確定で右側が空白なら優先して向きを変える(確定)
(2) 前が空白ならそのまま確定
(3) 右も前も埋まっているなら左に向きを変える(未確定)

単なる画像処理なので探索点はいくつでも構わないし、何千個
あっても、数万個あっても処理速度はほぼ一定です。
動的分岐とリソースアクセス遅延によるばらつきが出る可能性が
ありますが、ほぼ完全に面積だけで速度が決定します。

複数同時に動く可能性があるので、衝突判定はかなり厳密です。
Shader が入力可能なのは 1つ前のシーンだけなので、同じ空白を
複数のピクセルが同時に移動可能と判断してしまう可能性が
あるからです。

この同期サイクルのせいで、1pixel 歩くまでに 3フレームも
費やしています。迷路生成よりも判定が長いのは、確定判定が
追加されたからです。まず maze と違い足跡を正確に消す必要が
あります。さらに右を向いた瞬間もと来た場所が空きになるので、
さらに右手に回れると判断してしまう(戻ってしまう)ことも防いでいます。

これらの判定のおかげでピクセルが同時にいっぺんに移動しても
問題なく処理可能となりました。すり抜けも起こらないでしょう。

迷路の生成 maze.fx は ss03 と同じものですが、乱数計算に
偏りのバグがあったので一部修正しました。これはそのまま
ss03 で使うことも出来ます。


ダウンロードはこちらです。
wheelhandle_ss04t.zip
ソースだけでなく実行ファイルも入っています。


2D 迷路を作成する Shader を作ってみました。
これは実際に見た方が早いと思うので、環境がある方はぜひ走らせて、
動いているところをご覧ください。

64x64 で作成中
maze 64x64

512x512 で作成中の一部
maze 512x512

前のフレームの画像をもとに Shader で発展させていくだけで迷路が
出来上がります。

同時に移動した場合の成長点同士の衝突判定を行う都合上、壁が 1dot
成長するために 2フレームかかります。
その代わり平行していくつでも成長点を設けることができ、
同時に複数の壁を伸ばすことができます。
画像1枚をレンダリングしているだけなので、成長点がいくつあっても
処理速度はほぼ一定です。

アーカイブ内の initdata.png が初期画像です。任意のサイズを与える
ことができます。また白黒2値の絵を描いておくとそれをもとに発展
させます。最外周には必ず白枠が必要です。

生成処理は PixelShader 1つで完結しています。
最適化は全くしておらず、命令数は 625 slot もあります。
ほとんど if 文の塊で、調べたら動的分岐が 9段もネストしていました。
temp register を 27個も使っているので並列度も低いです。
ループを unroll しているのは最適化のためではなく、ループを中断
終了するためです。

sampler を使わずに Buffer から読み込んで整数処理すれば、条件判断
も簡略化できるためずっと小さくなるでしょう。

乱数テーブルのみ CPU で生成しています。本当はこれもシェーダーで
やるべきところなのですが今回は見送りました。

成長点はランダムで壁が変質して出来ます。各ピクセルの意味は R で
判断しており、次の 4つの状態があります。

R = 状態
	0.0  = 空白 (黒)
	1.0  = 固定壁 (白)
	0.5  = 成長点 (壁相当)
	0.25 = 成長予約席 (空白相当)

成長点のみ G B A も意味を持ちます。

R = 0.5 (成長点)
G = 向き (0.0~0.25=上, 0.25~0.50=下, 0.50~0.75=左, 0.75~1.00=右)
B = 直進制限長
A = 回転タイムアウト


最後になかなか埋まらない隙間が出来るのは壁から成長点を作る判定が
単なる乱数だからです。全く発展できない場所にもどんどん成長点を
作ってしまい、それがちらついてノイズに見えてしまいます。
必要な場所に生成して、不要な場所には成長点を作らないようにすれば
隙間も埋まるしちらつきも減ると思います。
成長点の生成ノイズが縞々に見えるのは、乱数の質が悪い証拠です。

実行ファイルとソースのアーカイブはこちら。シェーダーも入ってます。
wheelhandle_ss03t.zip

動作には Vista + DirectX10 (August2007 Runtime) が必要です。


あまり実用性が無いサンプルですが、Shader で HELLO WORLD を
作ってみました。

ss02t

ポイントは直接 Buffer に strcpy で文字列を書き込んでいるところ。
つまり Shader で文字列をそのまま表示しています。

描画文字列の書き込み
const char*	Message= "HELLO WORLD!";

g_iStringBuffer->Map( D3D10_MAP_WRITE_DISCARD, 0, &ptr );
strcpy( ptr, Message );	// ←ここ
g_iStringBuffer->Unmap();

描画文字列の表示も、Draw() に strlen() した文字列長をそのまま
渡しているだけです。

g_iDevice->Draw( strlen( Message ), 0 );	// ←ここ

StringBuffer は Shader の IA に渡しています。つまり Vertex 相当です。
1文字が 1頂点で、書き込まれているのはアスキーコードのみ。
stride size (1頂点のサイズ) はわずか 1byte です。

UINT	vSize= sizeof(char);	// ←ここ
UINT	vOffset= 0;
g_iDevice->IASetVertexBuffers( 0, 1, &g_iStringBuffer, &vSize, &vOffset )

InputLayout も DXGI_FORMAT_R8_UINT の1つだけ。

static const D3D10_INPUT_ELEMENT_DESC	_inputDesc[]= {
{ "CODE", 0,  DXGI_FORMAT_R8_UINT, 0, 0, D3D10_INPUT_PER_VERTEX_DATA, 0, },
};

もちろん Message は任意の文字列にできます。
が、付属の適当フォントがだめだめなので小文字とか はみ出します。
もう少し実用的にするなら D3D10_MAP_WRITE_DISCARD でなく
D3D10_MAP_WRITE_NO_OVERWRITE にして、バッファ管理
してあげる必要があるでしょう。
あとシェーダーでごまかしていますが、ステート周りなども未設定です。
(終了も手抜き)

詳しくは下記ソースをご覧ください。
wheelhandle_ss02t.zip

D3D10/DX10 の ジオメトリシェーダーに頼りまくってます。


ShaderModel 4.0 では、Shader 内で Texture の画像サイズを簡単に
参照することができます。自前でフィルタリングする場合など、UV 値の
計算に使えます。
3.0 以前のように、わざわざテクスチャサイズを外部から constant で
渡さなくてすむわけです。

取得するには Texture のオブジェクトに対して GetDimensions します。
August 2007 SDK のマニュアルを見るとちょっと説明が不完全で、
引数も足りず使い方もいまいちよくわかりません。メソッド名も微妙に
間違っているようです。

実際にコンパイルしてエラーメッセージから調べてみました。
GetDimensions() が正しい名前で、後述するいくつかのオーバーロードが
あるようです。
例えば Texture2D の場合次のようになります。

Texture2D    diffuseTexture;
  :

float4 PS_Main( GS_OUTPUT In ) : SV_Target
{
    float2 size;
    float  level;
    diffuseTexture.GetDimensions( 0, size.x, size.y, level );
     :
}

最初の 0 は参照する MipLevel です。
次の2つが受け取る width と height で、texture の pixel サイズに
相当します。例えば 1024 x 1024 のテクスチャを渡すと、

 size.xy= float2( 1024, 1024 )

が入ります。最後は MipLevel の個数が返ります。

// Texture2D の場合
GetDimensions( in uint level, out uint width, out uint height, out uint levels );
GetDimensions( in uint level, out float width, out float height, out float levels );
GetDimensions( out uint width, out uint height );
GetDimensions( out float width, out float height );

2D の場合上記 4つのオーバーロードがあり、uint でも受け取れるし
miplevel が不要なら width と height だけ受け取ることもできるようです。
2D 以外は下記のとおり。

// Texture1D の場合
GetDimensions( in uint level, out uint width, out uint levels );
GetDimensions( in uint level, out float width, out float levels );
GetDimensions( out uint width );
GetDimensions( out float width );

// Texture3D の場合
GetDimensions( in uint level, out uint width, out uint height, out uint depth, out uint levels );
GetDimensions( in uint level, out float width, out float height, out float depth, out float levels );
GetDimensions( out uint width, out uint height, out uint depth );
GetDimensions( out float width, out float height, out float depth );

// TextureCube の場合
GetDimensions( in uint level, out uint width, out uint height, out uint levels );
GetDimensions( in uint level, out float width, out float height, out float levels );
GetDimensions( out uint width, out uint height );
GetDimensions( out float width, out float height );

Cube にも height がありますね。事実上 2DArray 相当だからでしょうか。


昨日の記事で Quadro FX 1700 と 570 の違いに関して
 >おそらく Core/Shader クロックでも違いがあるのでしょう。
こんなことを書きましたが間違いでした。
すみません訂正いたします。
ELSA の製品情報を見たら core クロックは同一でした。

ELSA NVIDIA Quadro シリーズ

なので、おそらく sp 数による性能の差別化だと思われます。


ようやく ShaderModel 4.0 対応 Quadro の 1000系がでてきました。
NVIDIA、ミッドレンジ/エントリー向け「Quadro FX」のラインナップを拡充

1000系はメモリが少なかったので、しっかり 512MB あるのはいい感じです。
ただ 1700/570 も全体的にメモリの転送性能が下がっていますね。
メモリだけ見ると Shader3.0 時代の Quadro の方がバス幅も転送レートも
高かったようです。

そのため記事のスペックだけ見てると、一見 570 との差が少なくなり
FX570 がお得に見えてしまいます。でも公式の比較表をみると
実行性能でもきちんと差別化がはかられているようです。
おそらく Core/Shader クロックでも違いがあるのでしょう。

NVIDIA Quadro FX Product Comparison

この上の FX 4600/5600 とは性能も(値段も)差がありすぎるので、
3000番台も復活してくれるといいのですが。

HYPERでんちの GPU 表も更新しました。
DirectX10 GPU

表を見て気がついた疑問。Desktop の Quadro FX1700 や FX570
よりも Mobile の Quadro FX1600M や FX570M の方が転送レートが
高いのはなぜだろう。



ブエルタ、やっとペタッキが勝った!
復活の勝利、ペタッキがブエルタ通算18勝目を飾る


Autodesk Maya 2008 来ました。
保守に入っていればすぐにダウンロードすることができます。
これまでは 7.0、8.0、8.5 のようにナンバリングされていましたが
今回から 2008 です。略して Maya08。Maya8.0 に似てますが違います。

インストールフォルダも 2008 になりました。
起動時の Output Window にはまだ Maya9.0 と表示されています。
内部的には本当は Version 9.0 ?・・いえいえ
API の Version 番号は一気に増えて 200800 になっていました。
以前は ~ 700、800、850 だったので、内部的にも本当に 2008 に
なっているみたいです。

 API 850 → 200800

いろんな機能追加があると思いますが、D3D 関連だと

・mentalray の dds 入力サポート
・hlslShader の追加

あたりが主なトピックでしょうか。
Maya 自体は dds に対応していたものの mentalray では使えなかった
ので、うっかり dds を貼ったシーンをそのままレンダリングして
落ちたり・・。それを回避するために事前に texture path を置き
換える mel を作ったり、なんてことが過去にはありました。

ビューポートで描画できるハードウエアシェーダーは 4.5 あたりから
ついており、今までも CgFx 等を使って表示させることができました。
今度は HLSL の対応です。Direct3D の Effect (fx) フォーマットの
ファイルを読み込み、マテリアルとして適用することができます。
実際に使ってみました。

 ・Plug-in マネージャーで hlslShader.mll を有効にする
 ・Panel の Renderer を Default (High Quality でない) にする
 ・Panel の Shading から Hardware Textureing を ON にする

これで hlslShader のノードを作って描画確認できます。

とりあえず
C:\Program Files\Autodesk\Maya2008\devkit\plug-ins\
に入っている、Maya_fixedFunction.fx を読み込ませると簡単な
描画テストができるようです。

CgFx と違い、OpenGL ではなく本当に Direct3D を使っています。
ついに Maya 上で Direct3D を扱えるようになったわけですね。
API 的には class MD3D9Render が追加されており、ここから
IDirect3DDevice9 のインターフェースを取得することができるようです。

基本的には

World
WorldInverse
View
ViewInverse
Projection
WorldView
WorldViewProjection

などの古くから使われている一般的な Semantic に対応しています。
また Annotation を使った DXSAS にも対応しているようです。
(DXSAS = DirectX Standard Annotations and Semantics)
ただし Maya 内のライトとのリンクはまだ行われておらず、
シェーダーに入力されるパラメータの一種として設定しなければ
ならないようです。

実際に hlslShader を使いながらの開発はまだしないかもしれま
せんが、D3D のインターフェースを直接扱えるようになった点は
かなり大きな変化だと思います。

残念ながら D3D10 ではありません。Direct3D 10 だと Vista に
限定されてしまうし、まだ仕方ないのかもしれません。


Buffer への書き込みに DISCARD + Map() を使うことで、ドライバが
バッファの Rename を行うことができます。つまりアプリケーション
側では同一のバッファのつもりでも、実際はぜんぜん違うメモリに
書き込んでいたりするわけです。
これは最適化のためで、フレームバッファと同じようにドライバ側で
勝手に SwapChain 化してくれるようなもの。

その代わり Rename 数の上限によってバッファが枯渇する可能性が
あるそうです。(関連は下記エントリ)
Gamefest Japan 2007 (2) Direct3D 10

この Rename の状況を軽く調べてみました。


●同じバッファが割り当てられているかどうか確認する方法

(1) Map() した時のアドレス値 (*ppData の値) を比較する。
(2) D3D10_MAP_WRITE_DISCARD かつ D3D10_CPU_ACCESS_WRITEのみ
  であるにもかかわらず、敢えて前回書き込んだ値を読み出してみる。

(1) だけでも十分かもしれませんが念のため (2) も調べました。


●条件

ConstantBuffer を作成してみます。Map() でアクセスするには
下記の指定が必要です。

 ・D3D10_USAGE_DYNAMIC
 ・D3D10_CPU_ACCESS_WRITE
 ・D3D10_BIND_CONSTANT_BUFFER

使用した GPU は GeForce8800GTS です。


●未使用バッファ 16byte

16byte のバッファを作って、毎フレーム Map() で書き込んでみました。
このバッファは特に ~SetConstantBuffer() していないので、Busy に
なることはないはずです。

左から
・アドレス
・Bufferサイズ
・これから書き込む値
・書き込み前に読み出したメモリの値
と並んでいます。

addr     size    write     read
073eb400 16byte (00000000) 00000000
073eb800 16byte (00000001) 00000000
073eba00 16byte (00000002) 00000000
073ebc00 16byte (00000003) 00000000
073ebe00 16byte (00000004) 00000000
073ec000 16byte (00000005) 00000000
073ec200 16byte (00000006) 00000000
073ec400 16byte (00000007) 00000000 (A)
073ec600 16byte (00000008) 00000000
~
073f4600 16byte (00000042) 00000000
073f4800 16byte (00000043) 00000000
073ec400 16byte (00000044) 00000007 (B) 一周 (A)で書き込んだ 7
073ec600 16byte (00000045) 00000008
~
073f4600 16byte (0000007f) 00000042
073f4800 16byte (00000080) 00000043
073ec400 16byte (00000081) 00000044 (C) 一周 (B)で書き込んだ 44
073ec600 16byte (00000082) 00000045
073ec800 16byte (00000083) 00000046
~

アドレスは 512byte 単位で割り振られています。60個程度のバッファが
順次割り当てられ、繰り返し利用されているように見えます。
未使用バッファでも毎回違うアドレスになるのは予想外でした。


●未使用バッファ 512byte

512byte のバッファではずれが小さくなっています。どちらにしろ
未使用バッファなのでたまたまかもしれません。

addr     size     write     read
0727b600 512byte (00000000) 00000000
0727b800 512byte (00000001) 00000000
0727b600 512byte (00000002) 00000000
0727b800 512byte (00000003) 00000001
0727b600 512byte (00000004) 00000002
~


●実バッファ 1088byte

実際に ~SetConstantBuffer() してシェーダー参照されている
描画に使っているバッファを見てみます。

    addr     size   write read
*L1 0754b000 1088byte (0) 0
*L1 0754b000 1088byte (1) 1
*L1 0754b000 1088byte (2) 2
*L1 0754b000 1088byte (3) 3
*L1 0754b000 1088byte (4) 4
~

ここでの write は、1つ前のフレームで書き込んだ値です。
アドレスも、前回書き込んだ値も一致しています。
同じバッファが割り当てられています。
このプログラムは厳密に CPU と GPU の同期を管理しているため、
次の Map() までに描画が完了してしまっている可能性があります。


●実バッファ 1088byte ×2

1フレーム中、同じバッファに2度転送を行ってみました。
L0/L1 のペアで1フレームです。

    addr     size   write read
*L0 0735b000 1088byte (0) 0
*L1 0735b500 1088byte (1) 0
*L0 0735b000 1088byte (2) 1
*L1 0735b500 1088byte (3) 2
*L0 0735b000 1088byte (4) 3
*L1 0735b500 1088byte (5) 4
~

今度はそれぞれ異なるバッファが割り当てられています。
参照されている Buffer は実際の描画が完了するまで Busy に
なり、アクセスから保護されている様子がわかります。
やっと予想した動作が見えてきました。


●実バッファ 1088byte ×20

今度は1フレーム中、同じバッファに20回書き込みます。

    addr     size   write read
*L0 0719b000 1088byte (0) 0
*L1 0719b500 1088byte (1) 0
*L2 0719ba00 1088byte (2) 0
~
*L17 071a0500 1088byte (17) 0
*L18 071a0a00 1088byte (18) 0
*L19 071a0f00 1088byte (19) 0
*L0 0719b000 1088byte (20) 1
*L1 0719b500 1088byte (21) 2
*L2 0719ba00 1088byte (22) 3
~

Rename の様子が良くわかります。
ちなみに各 Map() と Map() の間に Draw を挟んでいないので、
本来は最後の Map() 以外は破棄してかまわない状況です。
今のところ Map( DISCARD ) を実行した数分だけバッファが
割り当てられているようです。
Busy の情報はリネーム元のバッファ1つ分しか持っていない
のでしょうか。


●実バッファ 1088byte ×20000

2000回では変化無かったので、一気に 20000回行きます。

*L0 071fb000 1088byte (0) 0
*L1 071fb500 1088byte (1) 0
~
*L3916 0aee5c00 1088byte (3916) 0
*L3917 0aee6100 1088byte (3917) 0
*L3918 071fb000 1088byte (3918) 1
*L3919 071fb500 1088byte (3919) 2
*L3920 071fba00 1088byte (3920) 3
~

20000回を待たずに再利用されています。
やっぱり本当は使っていないことがばれているのかもしれません。
ここまでで考えられるバッファサイズはおよそ 8Mbyte でしょうか。
動きは見えましたが、完全に捕まえるためにはきちんとテスト
プログラムを作った方がよさそうです。ちょっと試したくらいでは
まだわからないですね。


引き続き Microsoft Gamefest Japan 2007 の話題です。
関連エントリ
Gamefest Japan 2007 (1) Direct3D 10 と Meltdown
Microsoft Gamefest Japan 2007

Direct3D 10 に直接関連するのは下記の 2コマです。

1. Windows Vista グラフィックス 開発の手ほどき: Direct3D 10 and 10.1
2. Windows to Reality : D3D10 グラフィックスのゲームへの投入

メインはたぶん 2. の方でしょう。内容は、D3D9 から D3D10 へ
の移行を促すためのアドバイスです。

・9 から 10 に移行しても API を変えただけでは必ずしも速くならないこと
・どうして API や仕組みを変えたのか
・理由を踏まえてどう使えばいいのか
・Direct3D9 から移行しつつ 10 で想定した使い方を引き出す手法

とはいえ Direct3D10 の SDK に触れたことが無いと、少々
ぴんとこない内容だったかもしれません。
そのため前提となる知識として、Direct3D10 を解説した 1. の
「Windows Vista グラフィックス 開発の手ほどき: Direct3D 10 and 10.1」
が入ります。

逆にすでに D3D10 上で設計し、実際に開発をしている人に
とっては、移行時のテクニックも不要なので参考になる点は
少なかったかもしれません。
現在 D3D9 を使っている人、または移行中の人がターゲットです。


D3D9 から D3D10 への設計の変更や API の大きな変更は、
技術者が新たに使い方を覚え直す必要があり、また移植性や
ソースの互換性を失うリスクがあります。
それでもやる価値があると判断したわけで、その一番の目的は
パフォーマンスの向上でした。

ところが D3D10 の機能変更の意義と、新しい設計の意味を
きちんと理解して使わなければパフォーマンスがさっぱり
あがらなかったものと考えられます。
つまり単なる移植で D3D9 から機械的に API を置き換えた
だけではだめだということです。

そのポイントをいくつか絞って紹介しており、比較的大きな
変更をしなくても D3D10 の設計思想に合わせられる折衷案も
合わせて提示されています。

最終的な結論は、やっぱり D3D10 の機能を活用し、
そのために新たに設計しなおすことが一番とのこと。


以下いくつか気になった点など

バッファを Map(Lock) して CPU がアクセスする場合、
DISCARD や NO_OVERWRITE 指定は高速だということが良く知ら
れています。その理由は GPU/CPU が同期待ちをする必要が
無いからで、特に DISCARD はドライバが必要に応じて
Buffer Rename を行います。

この Rename 数には上限があって、バッファが枯渇する
可能性があることが述べられていました。
なるほど、そこまで考えたことはありませんでした。
ConstantBuffer は DISCARD アクセスしかできないので、
更新手段を選択する判断材料になりそうです。

UpdateSubresourece() に関する説明は、メモリコピーの負荷が
アプリケーション側しか考慮していないように見えたので
少々わかりにくかったかもしれません。

Clear~() 系はフレームバッファ全体なので、ステートに
基づいたクリアが必要ならシェーダーを作ってポリゴンを
書けとのこと。

cbuffer より tbuffer の方がランダムアクセスに適している
場合があるそうです。index によるアクセスなど。
これは意外でした。
そもそも cbuffer が速くないのか、
インデックスのアドレッシングが苦手なのか、
texture cache のおかげなのか、
いつか検証してみたいポイントです。

[unroll(n)] は n 回までのアンロールを強制して、最適化
のヒントに使える、と書かれています。
でも以前試した限りでは、回数指定をつけると n 回で
ループ自体を打ち切ってしまうのでプログラムの動作その
ものが変化してしまいます。もしかしたらこれは意図した
動作では無いのかもしれません。

ConstantBuffer は自前で管理せよとのこと。
この辺は構造からしてやっぱり、といった感じ。

Direct3D 10.1 に関しては特に目新しい情報はありませんでした。
ShaderModel4.1 の変更は思ったより小さいかもしれません。
単に 10.1 用の新しいリソース命令が追加されただけ、とか
十分ありえます。ps1.0→1.3 と同じような感じで。


Microsoft の Gamefest Japan 2007 に参加してきました。
Direct3D 10 に直接関連するセッションは 2つ。

2007/09/06
1. Windows Vista グラフィックス 開発の手ほどき: Direct3D 10 and 10.1
2. Windows to Reality : D3D10 グラフィックスのゲームへの投入

DirectX のイベントといえば Meltdown です。
ここしばらくの間、Meltdown は単独開催ではなく CEDEC 協賛
セッションとして行われてきた印象があります。
調べてみました。
実際に Meltdown という名前で CEDEC の中で行われたのは
2回だけだったようです。

●Meltdown Tokyo 1997
●Meltdown Tokyo 98 SUMMER
●Meltdown Tokyo 1999
●Meltdown Tokyo 2000
●Meltdown Tokyo 2001
●CEDEC 2002 DirectX Day
 http://www.microsoft.com/japan/msdn/directx/techarts_dx8.aspx
●Meltdown in CEDEC 2003
 http://www.microsoft.com/japan/msdn/directx/meltdown2003/default.aspx
●CEDEC Meltdown 2004
●Micrtosoft Game Developers Day CEDEC 2005
●2006 無し

Meltdown の初期のころは互換性テストが目的で、developer
は各社の新しいビデオカードにいち早く触れる良い機会でも
ありました。
初参加はたぶん 1997 頃だと思います。Mystique や
PERMEDIA2 など、DirectX にあわせた 2世代目 GPU が揃って
登場してきたあたりです。

開発中でまだドライバが不安定なものもありましたが、
それまで使ってきたビデオカードと比べると、どれも非常に
パフォーマンスがあがっています。
そして Direct3D との相性もよくなり、対応機能が増えている
ことに素直に感動してました。
が、それも最後のブースを見るまでのこと。

たまたま最後にスケジューリングしていたのは、当時はまだ
無名だった NVIDIA です。横倒しで机の上にむき出しのまま
組まれた PC に不安を覚えながら、プロトタイプの RIVA128 で
手持ちのプログラムを起動してびっくり。
口からでた感想は一言 「今までで一番速い」。


CEDEC と一緒になってからは、互換性よりも新しい概念である
shader をどう活用するのか、という点にフォーカスが変わります。
ちょうど shader によってリアルタイムCG は非常にホットな
時代を迎えています。
必然的に現行または将来可能となる技術的な内容が多く、
トレンドを反映した高度な shader 論と、急激に変化する
API や GPU 機能の解説が多くなります。
密度の高い内容となりました。


多少落ち着いてきた 2004 年あたりからは、また若干方向に
変化が見られます。急に高度化した内容に対するより戻しか、
開発者向けの救済路線が徐々に登場してきます。
XNA というキーワードが出たものの一体それは何なのか、
ミドルウエアなのか、リソースマネージャなのか、
グループウエアなのか。
「開発コストを下げるため」の1点以外良くわからない混乱の
時期でもありました。


XNA の路線が固まってからは現状のとおりです。
Gamefest Japan 2007 もこの流れを反映しており、おそらく
開催の中心は XNA にあったものと思われます。

最初にあげた Direct3D 10 の 2つのセッションに関しては、
未経験者に対する API や違いの紹介、Direct3D 9 から
移行する方への変化の説明、乗り換え時の注意点等といった、
ポイントを絞った最小限のものとなっていました。
表現など shader 技術的な話ではなく、まずは API の乗換えが
最重要課題なのかもしれません。

内容については次回でもう少し詳しく書いてみます。


前回の続きです。
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) の変換マトリクスを、
法線と同じように頂点単位で格納しておきます。回転なので
3x3 でよく、これはオブジェクト空間における接空間の 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系ツールが吐き出すノーマルマップには、互換性が無い
ことが多いのです。

 ・スムース補間問題
 ・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 のノーマルマップを貼った方が実現は簡単です。


●ゲームの従来の手法

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

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

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

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

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

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

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


●ノーマルマップの威力

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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


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


再び HLSL (Effect) のコンパイル関連です。
Direct3D 10 Shader4.0 APIによってコンパイラが違う
で少々調べたので、もう少し説明してみます。
マニュアルに詳しく書かれていないこと、わかりにくいこと、
実際に試したことなどなどをメモしていきます。

API まとめ
・Compile 系 API  HLSL/fx → Blob のバイトコード

ID3D10Effect/D3D10CompileEffectFromMemory()
ID3D10Shader/D3D10CompileShader()
D3DX10/D3DX10CompileFromFile()
D3DX10/D3DX10CompileFromMemory()
D3DX10/D3DX10CompileFromResource()
   
・Create 系 API  バイトコード → インターフェース

ID3D10Device::CreateVertexShader()
ID3D10Device::CreatePixelShader()
ID3D10Device::CreateGeometryShader()
ID3D10Device::CreateGeometryShaderWithStreamOutput()
ID3D10Effect/D3D10CreateEffectFromMemory()
ID3D10Effect/D3D10CreateEffectPoolFromMemory()
D3DX10/D3DX10CreateEffectFromFile()
D3DX10/D3DX10CreateEffectFromMemory()
D3DX10/D3DX10CreateEffectFromResource()
D3DX10/D3DX10CreateEffectPoolFromFile()
D3DX10/D3DX10CreateEffectPoolFromMemory()
D3DX10/D3DX10CreateEffectPoolFromResource()
D3DX10/D3DX10CreateAsync~

・Disassemble 系 API  インターフェース → Blob の disassemble リスト

ID3D10Shader/D3D10DisassembleShader()
ID3D10Effect/D3D10DisassembleEffect()
D3DX10/D3DX10DisassembleShader()
D3DX10/D3DX10DisassembleEffect()

・Preprocessor 系 API  HLSL/fx → Blob のテキスト

ID3D10Shader/D3D10PreprocessShader()
D3DX10/D3DX10PreprocessShaderFromFile()
D3DX10/D3DX10PreprocessShaderFromMemory()
D3DX10/D3DX10PreprocessShaderFromResource()

必要な機能がほとんどそろっています。fxc.exe はコマンド
ライン引数のパースだけで、あとは D3DX10 を呼び出している
だけのようです。


D3DX10 の D3DX10Create~ は HLSL からのコンパイルだけでなく、
コンパイル済みデータ(fxo)を受け取ることも出来ます。


Preprocessor は Include や Macro 等のテキスト処理を行い、
結果を返してくれる便利な関数です。まず、シェーダー
コンパイル時にマクロの適用がきちんと行われているか確認
することができます。

またシェーダーにこだわらず、script 系言語の汎用のテキスト
処理フィルタとして流用できるかもしれません。

ただ一旦パーサにかけたテキストを再構築して出力している
らしく、無駄な空白や空行が除去され各キーワードがスペース
区切りに置き換わっています。
わかりやすいし処理しやすいものの、#line 等のディレクティブも
失われてしまいます。
本当のプリプロセッサとして活用する場合は、エラー時の行番号
を求めることが出来ず、デバッグしづらくなるのが難点でしょう。


Disassemble/Preprocessor など Blob のテキストで返す API は
'\0' 終端でかつ、ID3D10Blob::GetBufferSize() が '\0' を
含めたサイズを返す点に注意です。

例えばマニュアルの D3D10DisassembleEffect の説明には下記の
プログラム例が載っています。(下記はマニュアルからの引用)

LPCSTR commentString = NULL;
ID3D10Blob* pIDisassembly = NULL;
char* pDisassembly = NULL;
if( pVSBuf )
{
    D3D10DisassembleEffect( (UINT*)
    	l_pBlob_Effect->GetBufferPointer(),
        l_pBlob_Effect->GetBufferSize(), TRUE, commentString,
	&pIDisassembly );
    if( pIDisassembly )
    {
        FILE* pFile = fopen( "effect.htm", "w" );
        if( pFile)
        {
            fputs( (char*)pIDisassembly->GetBufferPointer(), pFile );
            fclose( pFile );
        }
    }
}

もしこれを次のように書くと、テキストファイルの終端に '\0' が
含まれてしまいます。
(__Write は指定バイト数ファイルに書き込む仮想 API とする)

__Write( pIDisassembly->GetBufferPointer(),
		pIDisassembly->GetBufferSize() );



D3DX10CompileFromFile() は開始関数名の指定があるので、一見
Shader コンパイル専用の関数に見えます。実際は Effect(fx) の
コンパイルも可能です。この場合関数名 pFunctionName には NULL
を渡します。


Compile 系 API に渡す D3D10_SHADER_MACRO は、#define ~ に
相当する定義リストを渡すためのものです。この定義リストは
定義内容を文字列で渡す必要があり、かつ NULL 終端です。
マニュアルの D3D10_SHADER_MACRO の記述例では size [1] の配列を
使っていてこの NULL 終端が含まれていないので注意です。

以下マニュアルより引用
D3D10_SHADER_MACRO Shader_Macros[1] = { "zero", "0"  };

実際に使う場合はこんな感じです。

D3D10_SHADER_MACRO Shader_Macros[]= {
	{  "zero", "0"  },
	{  "TEXLAYER", "2"  },
	{  "POINTLIGHT", "8"  },
	{  NULL, NULL  },
};


HYPERでんち の方もいくつか更新しました。
シェーダーの世代ごとの違い
DirectX SDK バージョン一覧