過去の独りごち/独りごとは こちら
過去のJavaアプレットは こちら

現在、BBSに書き込みキーを設けています。「書き込みキー」欄に、「MYOMOTO」をカギカッコは抜いて、 半角、小文字で 打ち込んで書き込みをしてください。 このキーワードは時々変更されますが、その都度こちらにて報告します。


8/20 sdPBRをつくりました

お久しぶりです。Twitterとニコニコ動画を主な居場所として半年以上が経ちました。その間にMikuMikuEffect用のエフェクトを山のようにこしらえて、 配布動画も一杯つくりました。

とりわけ大がかりなエフェクトだったのが最新作のsdPBRです。名前通りPBR(の僕なりの解釈)に則ってMMDモデルのレンダリングを行うシェーダです。

当初は作るつもりが無かったのですが、きっかけになった事がこちらの動画に書いてありまして

ええと、(僕個人としては)未知の画質を求めてパストレーシングに手を出したのはいいんですが、「ちゃんとしたシェーディング」というものがどういったものなのかよくわからないという事を再認識してしまったので、色々と探したところ、Disney principled BRDFという良い物が有ると知りまして、これをお手本にすればナウい画質が得られると、しめしめ。そう思ったのでちょっと試してみたんですね。

うん、これは良い!ただのカプセル型がなんとも言えないディズニー感を出しやがります。あんまりそう見えないかもしれませんけど、パストレーサの方に仮にちょっと組み込んでみただけでも、今までのLambertDiffuseにCook-Torranceスペキュラを意味も解らず足しこんでいただけのシェーディングより随分表現力のある物になりそうだと確信しました。こんな良い物がたったの数十行のコードで簡潔に書かれているなんて!

そんなわけで続く1~2か月ほどの間に舞力介入P氏のfull.fxをベースにしたコードを出発点として長い道のりを営々と歩みました。

  1. full.fxにDisney principled BRDFの組み込み
  2. HDRI skyboxによるIBL(diffuse,specular環境マップ)
  3. トーンマッパーの作成
  4. (この辺りで暁編公開)
  5. 法線マップ、ParallaxOcclusionマップの実装
  6. 追加の光源としてスポットライトの実装
  7. スポットライトへシャドウマップ追加
  8. スポットライトのIESプロファイル対応
  9. スポットライトへボリュームフォグ追加
  10. 平行光源へシャドウマップ・ボリュームフォグ追加
  11. ポストエフェクトとしてSSR(ScreenSpaceRelection)作成
  12. SSDO(ScreenSpaceDirectionalOcclusion with IndirectBounce)実装
  13. SphericalHarmonicsを用いてディフューズ環境マップを生成するようにした、hdr2sh.exe作成
  14. EyeAdaptationの実装
  15. ラフネスマップ、AOマップなど追加
  16. 負荷と画質の設定をプリセットから選べるようにsdPBRconfig.exe作成
  17. (Version 1.00として公開)

  18. pre-integrated skinシェーディングモデルの追加
  19. subsurfaceマップの追加
  20. 磯っぺ氏の協力により、狙いIKボーン付きスポットライト同梱
  21. sdUnsharpMask作成
  22. 違う種類のマップを1つのテクスチャのRGBA各チャンネルにパッキングできるようにした
  23. スポットライトのシャドウマップもRGBA各チャンネルを使ってパッキングした
  24. (version 1.10として公開)

  25. DayDynamicSkyboxの作成、うごくskyboxの誕生
  26. Kuraudo.pmxで雲を掛けられるようにした
  27. NightDynamicSkyboxの作成、夜版
  28. 平行光源のボリュームライトの品質向上と高速化で実用的になった
  29. (Version 1.20として公開)

  30. スペキュラの計算にずっとバグがあった事に気づいたので修正
  31. 平行光源のボリュームフォグに虹を付けられるようにした

  32. (Version 1.30として公開)

  33. sdSSRにフレネル項の計算をするオプションを付けた

箇条書きにすると長く険しい道のりだったと思いますが、まだ歩いている途中という。というか、最初は1.だけで終わるつもりだったんですよ、それがどうして…。1項目進めるのにも数本の文献を当たっていますから、読んだ記事や論文やコードの数は相当な数になります。readmeに書いてある参考文献は記事を丸ごと読んで全体的に参考になったとか、コードの一部を引用させていただいたり定数を採らせていただいたとかそういう物だけなので、実際に参考にした文献の本数はさらに何倍もあるような気がします。

作れば作っただけ勉強になって、分かること・出来る事が増えていくのが実感できるので、それだけの良さがPBRには有るという事ですね。特にシャドウマップは自分で組んでみないと分からない、サジ加減の要求される所が多く、取り組んでよかったと思います。レイトレ時代になれば…と思わなくもなかったのですが、その時代になってもまだ生かせるところはあるんじゃないかと思いました。

ところで、なぜ、MMDモデルを描くためのシェーダとして実装したのか、うごく背景用のシェーダに組み込まなかったのか、と言うとレイトレ・パストレベースのうごく背景用のシェーダに入れてしまうとDirectX9ベースのMMEでは定数レジスタが枯渇して、ジオメトリを作るためのコードにほとんど何も書けなくなったからです。

ともかく、スペキュラの計算ミスに起因するどうにも腑に落ちなかった大きなバグも取れたという事で、一息ついている所ですが、まだ色々と盛り込みたいところもありますし、sdPBRとは関係なく使える便利で気の利いたエフェクトも作りたいと思っています。早速sdPBRを作品作りに活かしてくださってる皆さん、動画へのご広告など、ご声援くださった皆さんありがとうございます。いつも励みにしております、ホントに。

おまけ

読んでいる方に何も役に立たない記事というのはなんだか申し訳ないのでおまけを書いておきます。題してMMEでレイトレーシングをやる方法といってもMME自体はDirectX9準拠なのでRTCoreなどの最新兵器は使えませんから、パフォーマンス上の問題とかコンパイル時間とかレジスタ数とか色々ありますけど、自分で色々試して落としどころを探るのが肝心です。あ、コードのサンプルはMITライセンスで公開しますから好き勝手に使ってください。MME入門記事ではないので、既にMME作ってるけどレイトレがやりてぇんだ、という方だけを対象にしています。ニッチだなぁおい!

それは置いといて、ポストエフェクトでレイトレーシングをやる方法を念頭に置いて解説しますが、色々と応用が利くと思いますが、まずはレイを生成しましょう。

void RaySaySay(float2 Tex, out float3 ray, out float3 pos, uniform bool normalizeFlag = true)
{
	float2 tex = (Tex.xy - 0.5) * 2.0;
	//normalizeをすると球面状にレイトレするようになり、しないと平面の板上をレイトレする
	//板上レイトレではSDFを使ったレイマーチには注意が必要
	ray =
		ViewMatrix._13_23_33 / ProjectionMatrix._33
		+ (ViewMatrix._11_21_31 * tex.x / ProjectionMatrix._11)
		- (ViewMatrix._12_22_32 * tex.y / ProjectionMatrix._22);
	if (normalizeFlag)
		ray = normalize(ray);
	pos = CameraPosition;
}

定義部分を飛ばしてグローバル変数が色々出てきますが、MMEを書いたことがある人なら大体何のことか分かると思うのでグローバル変数でいつものあいつらを宣言してください。

このRaySaySayという関数はピクセルシェーダから呼ばれることを念頭にしてまして、Texという引数でスクリーン座標(左上が(0,0)で右下が(1,1))を指定します。カメラからスクリーン座標めがけて飛び出すレイ(ray)をワールド空間での表現として生成することが出来ます。posにはカメラのワールド座標が返ります。

とりあえずこのRaySaySay関数でレイを作ることができるので、それを使ってわりとなんでもできます。

そんなわけでちょっと球体でも描いてみましょうか。球体はいいですよね。ポリゴンで完全な球は表現できないから謎の優越感があります。

//おまじない
float Script : STANDARDSGLOBAL <
	string ScriptOutput = "color";
	string ScriptClass = "scene";
	string ScriptOrder = "postprocess";
> = 0.8;

//いつものあいつら
float4x4 ViewMatrix	: VIEW;
float4x4 ProjectionMatrix : PROJECTION;
float3   CameraPosition    : POSITION < string Object = "Camera"; > ;
float3 LightDirection : DIRECTION < string Object = "Light"; >;
float3  LightAmbient      : AMBIENT   < string Object = "Light"; >;
static float3 LightColorLinear = pow(max(1e-4,LightAmbient),2.2);

float4 ClearColor  = float4(0,0,0,0);
float ClearDepth  = 1.0;

//ちょっと便利なビューポート関係の変数。ikeno氏のコードを参考にさせていただきました!
float2 ViewportSize : VIEWPORTPIXELSIZE;
static const float2 ViewportOffset = (float2(0.5,0.5)/ViewportSize);
static float ViewportAspect = ViewportSize.x / ViewportSize.y;

texture2D DepthBuffer : RENDERDEPTHSTENCILTARGET <
	float2 ViewPortRatio = {1.0,1.0};
	string Format = "D24S8";
>;

texture2D ScnMap : RENDERCOLORTARGET <
	float2 ViewPortRatio = {1.0,1.0};
	int MipLevels = 1;
	string Format = "D3DFMT_A16B16G16R16F";
>;
sampler2D ScnSamp = sampler_state {
	texture = ;
	MinFilter = LINEAR; MagFilter = LINEAR; MipFilter = NONE;
	AddressU  = WRAP; AddressV = WRAP;
};


struct VS_OUTPUT {
	float4 Pos			: POSITION;
	float2 Tex			: TEXCOORD0;
};

VS_OUTPUT VS( float4 Pos : POSITION, float2 Tex : TEXCOORD0 ) {
	VS_OUTPUT Out;
	Out.Pos = Pos;
	Out.Tex = Tex + ViewportOffset;
	return Out;
}

//ここが今回のキモです
void RaySaySay(float2 Tex, out float3 ray, out float3 pos, uniform bool normalizeFlag = true)
{
	float2 tex = (Tex.xy - 0.5) * 2.0;
	//normalizeをすると球面状にレイトレするようになり、しないと平面の板上をレイトレする
	//板上レイトレではSDFを使ったレイマーチには注意が必要
	ray =
		ViewMatrix._13_23_33 / ProjectionMatrix._33	//このZ軸方向の値に倍率を掛けると嘘画角効果をだせます
		+ (ViewMatrix._11_21_31 * tex.x / ProjectionMatrix._11)
		- (ViewMatrix._12_22_32 * tex.y / ProjectionMatrix._22);
	if (normalizeFlag)
		ray = normalize(ray);
	pos = CameraPosition;
}


//ぶっちゃけレイマーチよりも球と直線の交点の式使って解析的に求めた方が遥かに早いですが
//色々と応用が利くのでここではレイマーチを使ったコードを書いてます
//レイマーチング法そのものの解説はここではしませんから調べよう!
float4 PS( float2 Tex: TEXCOORD0) : COLOR
{
	float3 ray,pos;
	RaySaySay(Tex,ray,pos);	//レイ生成
	float3 O = {0,10,30};	//球の中心と半径。座標はMMDのワールド座標、長さもMMD単位です
	float R = 8;
	for (int i=0; i<50; i++) {	//レイマーチ
		float d = distance(pos,O)-R;
        if (d < 0.001) {	//小さい値になったらぶっかったとみなす
            float3 n = normalize(pos-O);	//Lambertianとしてライティング
			float c = saturate(dot(n,-normalize(LightDirection)))+0.05;//てきとうに環境色分を足す
			return float4(pow(c * LightColorLinear,1/2.2),1);	
        }
	pos += d * ray;	//レイの方向に進む
    }
	return tex2D(ScnSamp,Tex);	//背景色
}



//テック
technique postprocessTest <
	string Script =
	//MMD側の描画(おまじない)
	"RenderColorTarget0=ScnMap;"
	"RenderDepthStencilTarget=DepthBuffer;"
	"ClearSetColor=ClearColor;"
	"ClearSetDepth=ClearDepth;"
	"Clear=Color;"
	"Clear=Depth;"
	"ScriptExternal=Color;"

	//以下が今回の内容
	"RenderColorTarget0=;"
	"RenderDepthStencilTarget=;"
	"Pass=Draw;";
> {
	pass Draw < string Script= "Draw=Buffer;"; > {
		AlphaBlendEnable = FALSE;
		AlphaTestEnable = FALSE;
		VertexShader = compile vs_3_0 VS();
		PixelShader  = compile ps_3_0 PS();
	}
}

以上のコードをメモ帳とかにコピペして、raytore.fxとでも名前を付けてください。そしたら、ポストエフェクト用の枠になるraytore.xをどこかから持ってきます。拙作のsdPBRのposteffectフォルダに入ってるsdToneMapper.xとか、sdHaku.xとかをリネームして使っても良いです。あとはMMDにraytore.xをドロップすればOKです。

さて、できましたか?細かい文法ミスとかコピペミスがあったら直してね!今回は無駄にレイマーチングで球体を1こだけ描いてみました。

ちゃんとMMDの照明操作パネルの操作に従って色とか照らされる向きとかが変わります。律儀にリニア色空間で計算してますが、ここは譲れません。MMD内の他の物体との位置関係も一貫性が取れているんですが、前後関係を無視して球体が他の物体を隠して最前面に表示されるので、そこはデプスマップを作ったり色々して頑張ってね!




Hayase Taku(SANDMAN)

戻る