TGWS>プログラミング>

てきとうトゥーンシェード8.1

DirectX8.1を使って、トゥーンレンダリングのうち、今回はシェーディングだけをします。
それにあたって下記のサイトを大いに参考にさせていただきました。

イメージ画像(1084bytes)実はこれらのサイトのサンプルのサンプルをチュートリアルのプログラムにつぎはぎしていろいろ書き換えたものがこれなのですが。
そういうことで、ソースコードの解説は控えめにして、考え方の説明を中心に行っていきます。
左はイメージ画像です。これは普通にレンダリングしたのを減色しただけのものです。
なお、今回は知っていることを書いたのではなく、知らないことを手探りで探し求めながら、試行錯誤しつつ書いたものなので、間違ったことも書いてありますが、間違いだとわかったことはあとのほうで訂正していますので、読み始めたら最後まで読まないと誤解してしまう部分があるかもしれません。

まずは3D

通常レンダリング画像(3130bytes)何はともあれ3Dが表示できなければ話になりません。3Dを表示できるプログラムをとっとと作りましょう。
いまだにDirect3Dそのものがよく分かっていない私はSDKにあるサンプルを使うことにしました。ここで今回、ビューの位置などは、静止や自動でなく操作して移動できるようにしておきます。ついでにいうと、裏面も見えたほうが何かと都合が良いのでカリングは切っておきます。んでもって、法線の各成分は-1~1でないと困るので、NORMALIZENORMALSはオンにしておきます。
右に通常のレンダリング結果を示しておきます。画質が悪くなっていますが曲面の色が滑らかに変わっているのがわかると思います。

環境マッピングを使う

ライティングしない場合の画像(537bytes)こんな滑らかなグラデーションをさせないというのが今回の目的ですから、ライティングの設定なんて切ってしまいます。それが左の画像です。
今度は環境マッピングを使って影をつけます。簡単に考えるために光が真上から当たっていると考えると、曲面の法線が下を向いているときに黒く塗れば影になるということです。
そこで、下半分が黒いテクスチャを作るのですが、今回はテクスチャがどんな風にマッピングされるかを見るために諧調を増やしてあります。
シェーディング用テクスチャ画像(279bytes)作ったテクスチャと法線ベクトルを右図のように対応させます。法線の各成分の値は-1~1であり、テクスチャ座標は0~1なので、適当にスケーリングしてやります。

// 例
D3DXMatrixScaling( &matTrans1, 0.5f,-0.5f,1.0f );
D3DXMatrixTranslation( &matTrans2, 0.5f,0.5f,0 );
matTrans = matTrans1 * matTrans2;
g_pd3dDevice->SetTransform( D3DTS_TEXTURE0, &matTrans );

トゥーンシェード途中経過(1019bytes)で、これで、SetTextureStageStateを呼び出してD3DTSS_TEXCOORDINDEXを適当に指定してやって、ついでにD3DTSS_TEXTURETRANSFORMFLAGSも指定してレンダリングしてやると左のようになります。
上から見た画像(1325bytes)やー、こういう風に望みどおりの結果が出てきてくれると嬉しいものですね。そして、あんまり嬉しいのでいろんな角度から見てみたくなるものです。手動で動くようにしていた甲斐がありました。…んが、上から見たときに問題が発生します(右図)。
上から光が当たっているんだから、上から見たら真っ白になるはずなのに、何故でしょう。こちらから見れば確かに上のほうが白くなっていますけど…。
もう一度、もう一度冷静になって考えましょう。D3DTSS_TEXCOORDINDEXにはD3DTSS_TCI_CAMERASPACENORMAL指定したはずです。そうしないと横から見た図すらうまく表示されないはずですから。
カメラから見た各ベクトル(嘘)(1833bytes)要するにカメラから見た法線ということです。つまり、左図のように法線のZ成分はなくなってしまうので…しょうか?それを確かめるために簡単な実験を試みます。

// X,Y成分を消し、Z成分だけをV値に対応させる
D3DXMatrixRotationX( &matTrans0, -PI/2 );
D3DXMatrixScaling( &matTrans1, 0,0,0.5f );
D3DXMatrixTranslation( &matTrans2, 0,0,0.5f );
matTrans = matTrans1 * matTrans2 * matTrans0;
g_pd3dDevice->SetTransform( D3DTS_TEXTURE0, &matTrans );

法線のZ成分の有無を確かめる実験(1098bytes)で、実験の結果が右になります。Z値が法線のZ成分そのものであれば、こっちを向いているときには白で、向こうを向いているときには黒に見えます。図では、本体とふたの隙間からわずかに見えている部分が、向こうを向いた面がカリングされずに見えているもので、実際こっちを向いているときには白で、向こうを向いているときには黒に見えています。
ヘルプにこのことに関する記述を探してみたら、確かにありました。インターネットから見られるところでいきますと、MSDNライブラリの「カメラ空間トランスフォーム」のページに書いてあります。「N = norm(Nobject * (Mwv-1)T)」とのことです。法線のZ成分が消えるというのは私の勘違いだったようです。
さて、おかげで10行前後無駄にしてしまいましたが、得られるものもありました。ライトのベクトルにこの逆転置行列をかけてやればカメラ空間でもライトの方向になるわけです。これを正規化したものと例の法線の内積を取ってやればすなわちその部分の明るさというわけで、例によって適当にスケーリングしてやればめでたくこれでトゥーンシェーディングができるという計算です。
テクスチャ座標は、Vが内積にさえなれば他はどうでもいいので、2列目がライトの成分と同じ行列をかけます。

void ToonShade(){
	g_pd3dDevice->SetTexture( 0, g_pTeaTextures[0] );
	g_pd3dDevice->LightEnable( 0, FALSE );
	g_pd3dDevice->SetRenderState( D3DRS_LIGHTING, FALSE );
	D3DXMATRIX matTrans,matTrans0,matTrans1,matTrans2;
	D3DXVECTOR4 light1, light2;
	// ライト計算
	D3DXMatrixInverse(&matTrans1,NULL,&g_matView);
	D3DXMatrixTranspose(&matTrans2,&matTrans1);
	D3DXVec3Transform(&light1, &g_vecDir, &matTrans2);
	light1.w = 0;	// 4Dベクトルだと何かと都合が悪いのでW値を消去
	D3DXVec4Normalize(&light2,&light1);
	matTrans0 = D3DXMATRIX(
		1, light2.x, 0, 0,
		0, light2.y, 0, 0,
		0, light2.z, 1, 0,
		0, 0,        0, 1);
	// V値を0~1にスケーリング
	D3DXMatrixScaling( &matTrans1, 1.0f,-0.5f,1.0f );
	D3DXMatrixTranslation( &matTrans2, 0,0.5f,0 );
	matTrans = matTrans0 * matTrans1 * matTrans2;
	g_pd3dDevice->SetTransform( D3DTS_TEXTURE0, &matTrans );
	g_pd3dDevice->SetTextureStageState( 0, D3DTSS_TEXCOORDINDEX,D3DTSS_TCI_CAMERASPACENORMAL);
	g_pd3dDevice->SetTextureStageState( 0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_COUNT2 );
}

トゥーンレンダリングの失敗例(1189bytes)そして、できたコードがこれだあっ!そして実行(左図)。どきどき・・・
トゥーンレンダリング成功図(1185bytes)やるかと思ったら本当にやっちまいました。対処法はもはや言うまでもないので省略。成功図は右。
その後面白がってライトを動かしまくったのは言うまでもありません。

実際に使ったコード

元々のテクスチャを反映する(04/12/22追加)

前回はただ単にシェーディング用テクスチャを貼り付けただけだったから、元々テクスチャのあるメッシュ(さっきのコード例では虎)だとテクスチャが剥がれていました。
今回は元々テクスチャがあった場合はそれを反映するようにします。

頂点の色×影の色×テクスチャの色=出力する色(16811bytes)よーするに2つのテクスチャを掛け合わせて表示できればいいのです。
文字通り掛ければいいのです。掛け算です。MULTIPLYです!
だからSetTextureStageStateでD3DTSS_COLOROPにD3DTOP_MULTIPLYを…と言いたい所ですが、そんなのありません。
ごめんなさい知っててわざといいました。でもこれが私なんですあきらめなさい。
D3DTOP_MULTIPLYじゃなくてD3DTOP_MODULATEですね、はい。
まず、ですね。元々の色と影の色を合成します。
これは今までどおり。でも今回書いたコードでは普通のシェーディングと切り替えるためにちょっとした細工をしてあるんですが関係ありませんね、多分。
で、さらにそこにテクスチャを貼っ付けるんですね。

g_pd3dDevice->SetTextureStageState( 1, D3DTSS_COLOROP, D3DTOP_MODULATE);
g_pd3dDevice->SetTextureStageState( 1, D3DTSS_COLORARG1, D3DTA_TEXTURE);
g_pd3dDevice->SetTextureStageState( 1, D3DTSS_COLORARG2, D3DTA_CURRENT);
g_pd3dDevice->SetTexture( 1, g_pMeshTexture );

まあ、こんな感じ。多分。うまくいかなかったらごめん。

実際に作ったコード