順調みたいです。

こんばんわ、Pocolです。

お陰様で,今のところは”Direct3D12 ゲームグラフィックス実践ガイド”の売り上げが好調みたいです。
本について,読んでみて良かったら是非他の人にお勧めして欲しいのですが,その際に注意して欲しい点があります。
前々から言っているように,数学についての知識とC++についてついての知識をある程度必要とし,理系大学生の教養レベルの知識は必要となります。
また,自分がよかれと思って本書を奨めても,「全然良く分からないし,何なのこのクソ本!」と逆に自分の評価を下げてしまう可能性もあります。
本書を他者に進める場合も,その人の特性を見て薦めるかどうか判断して頂くと良いかと思います。
判断できない場合は,ちろっと読んで理解できなさそうであれば買うのをやめたが方がいいと言って頂いた方が良いです。専門書ですので…

まぁ,兎にかく合う合わないが個人によって非常に分かれる本だと思いますので,
現物を一回見て判断して頂くのが確実な方法かと思います。

個人的には,専門学校生には積極的におススメできない類の本です。
専門学校生の方は,DirectX12の魔導書などと比較してみて自分に合う方を買われると良いかと思います。

今年もありがとうございました。

こんばんわ。Pocolです。
本年度、最後の更新です。今年一年を振り返ろうと思います。

まずは,自分がシステム構築など行ったゲームが無事に世に出て良かったです。
ほとんどやったのはシステム構築だけで最後まで関われなかったので,
製品まできちんと出来たのは自分以外の人の努力の結晶だと思います。
間接的ながら,少しだけ仕事に関わることが出来て良かったと思います。
また,ミリオンも達成できて良かったと非常に思います。
いつも,エンジンやらライブラリやらしか作っていないので,
製品のスタッフロールに載ることは殆ど無いのですが… 
(XXX Team やら XXX 株式会社なりでまとめられてしまうので,名前が載ることはないのです)
今回はスタッフロールに載せて頂けたので,非常に良い経験となりました。

10月には,皆様に大変お待たせしましたが,ようやく執筆していた本を発売することができました。
レビューアの皆様ならび,編集担当の落合様には大変互助力をしていただきました。
この場で,再度お礼を申し上げます。ありがとうございました。
来年には,皆で打ち上げやりましょう!

後は最近うちのホームページなんか見ている人これっぽっちもいないだろうと思っていて,そろそろpublicなホームページは辞めたいなって思っています。
privateなページにして,のほほんとやっていきたい。自由に書きたい。
Githubに人知れずこっそりと記事をあげたりとか,今あんまり使っていないSlackとかで投稿するのも良いかもしれないなぁって感じています。
…ということで,そのうちページ閉じるかもという予告でした。

来年はどんな一年になるんでしょうか?
楽しい1年にしたいな。
それでは,皆様良いお年を!

Errata : Direct3D12ゲームグラフィック実践ガイド

大変申し訳ございません。
下記の通り、誤記がありましたので謹んでお詫びして訂正いたします。

P.245 中央部の数式
【誤】
\begin{eqnarray}
x &=& r \sin \theta \cos \theta \\
y &=& r \cos \theta \\
z &=& r \sin \theta \sin \theta
\end{eqnarray}

【正】
\begin{eqnarray}
x &=& r \sin \theta \sin \phi \\
y &=& r \cos \theta \\
z &=& r \sin \theta \cos \phi
\end{eqnarray}


P.245 図7.2
【誤】

【正】


P.341 リスト10.16
【誤】

auto invW = 1.0f / float(w - 1);
auto invH = 1.0f / float(h - 1);

【正】

auto invW = 1.0f / float(w);
auto invH = 1.0f / float(h);

サンプルプログラム Chapter.10 IESProfile.cpp
【誤】

auto idx = (w - 1) * y + x;

【正】

auto idx = w * y + x;

お、遅かったじゃないか…。

こんにちわ。Pocolです。
「お、遅かったじゃないか…。」

そう言われても仕方ありませんね。
Direct3D 12 ゲームグラフィックス実践ガイドの正式な発売が決定しました!
10月15日発売です!
三宅さんのAI本の一日後なので,三宅さんの本を買う人は一日遅らせて一緒に買いましょう。

本日より,先行販売している書店さんもあるようです。一日でも早く手に入れたいという方は,先行販売している書店さんに行ってみると良いかもしれません。

また,本日の先行販売に合わせて,書籍のサンプルプログラムのダウンロードページが公開されました。
下記ページの「本書のサポートページ」にアクセスするとzipファイルでサンプルプログラムがダウンロードできる状態になっています。
書籍のページ上の都合で,本にはプログラムコードがフルで書かれていませんので,こちらのサンプルを見ながら,本と照らし合わせて見て頂けると良いかと思います!

あと,ページレイアウト決まった後で,サンプルプログラムで未使用変数があるというビルド警告が出る箇所を見つけてしまったのですが,プログラムが実行できないなどの致命的な問題ではないのと,諸所の事情で修正できませんでした。
該当の箇所は,

Chapter9/Tonemap/Sample/res/TonemapPS.hlsl
107行目のL0
108行目のL1

です。
こちらはプログラム上で使っておりませんので,削除あるいはコメントアウトして頂けるとビルド警告が表示されなくなりますので,お手数ですが各自で修正をお願い致します。

…というわけで,来週発売になりますのでよろしくお願い致します!

私的メモ:モーションブラー

A Fast and Stable Feature-Aware Motion Blur Filterの疑似コード。

float cone(float dist, float r) {
	return saturate(1.0f - abs(dist) / r);
}

float cylinder(float dist, float r) {
		return sign(r - abs(dist)) * 0.5f + 0.5f;
}

// linear depth.
float zCompare(float za, float zb) {
		const float SOFT_Z_EXTENT = 0.1f;
		return saturate(1.0 - (za - ab) / SOFT_Z_EXTENT);
}

float3 MotionBlur(float2 p)
{
	// parameter setting (see. 5. Implementation and Results).
	const auto N		= 35;	// sample count.
	const auto eta		= 0.95; // a larger maximum jitter value (in pixel units). (see p.6)
	const auto phi		= 27;	// user-determined constant which affects the "baseline2 jitter level. (see p.6)
	const auto kappa	= 40;	// use-parameter to bais its importance. (see p.6)
	const auto r		= 40;	// a maximum image-space blur radius. (see p.2)
	const auto gamma	= 1.5;	// minimum user threshold (see p.4)

	auto j = Halton(-1, 1);

	// sOffset jitters a tile lookup (but never into a diagonal tile).
	auto vmax = FetchNeighborMax(p/r + sOffset(p, j));
	auto mag_vmax = length(vmax);
	if (mag_vmax <= 0.5f)
	{
		return FetchColor(p);
	}

	auto wn = vmax / mag_vmax;
	auto vc = FetchVelocity(p);
	auto wp = (-wn.y, wn.x); // vmax⊥.

	if (dot(wp, vc) < 0.0)
	{
		wp = -wp;
	}

	auto mag_vc = length(vc);
	auto wc = normalize(lerp(normalize(vc), wp, (mag_vc - 0.5) / gamma); // Eq. (1).

	// First integration samples: p with center weight
	auto totalWeight = N / (kappa * mag_vc);
	auto result = FetchColor(p) * totalWeight;
	auto j_dash = j * eta * phi / N;

	auto z_p = FetchDepth(p);

	for(int i=0; i<N; ++i)
	{
		auto t = lerp(-1.0, 1.0, (i+j_dash + 1)/(N+1)); // jitter sampler

		// Compute point S; split samples between {vmax, vc}
		auto d = (i & 0x1) ? vc : vmax; // iが奇数なら vc, iが偶数なら vmax.
		auto T = t * mag_vmax;
		auto S = int2(t * d) + p;

		// Compute S's velocity and color
		auto vs = FetchVelocity(S);
		auto colorSmaple = FetchColor(S);

		auto z_S = FetchDepth(S);

		// Fore-vs. background classification Y w.r.t p
		auto f = zCompare(z_p, z_S);
		auto b = zCompare(z_S, z_p);

		// Sample weight and velocity-aware factors (Sec.4.1)
		// The length of v_s is clamped to 0.5 minimum during normalization
		auto weight = 0;
		auto wA = dot(wc, d);
		auto wB = dot(normalize(vs), d);

		auto mag_vs = length(vs);

		// 3 phenomenological cases (Sec. 3, 4.1): Object
		// moving over p, p's blurred motion, & their blending.
		weight += dot(f, cone(T, 1 / mag_vs)) * wB;
		weight += dot(b, cone(T, 1 / mag_vc)) * wA;
		weight += cylinder(T, min(mag_vs, mag_vc)) * max(wA, wB) * 2;

		totalWeight += weight; // For normalization
		result += colorSample * weight;
	}

	return result / totalWeight;
}

McGuireのG3Dエンジンでは論文実装よりも変更がある。以下のように内積計算部分がガッツリなくなっている。
https://sourceforge.net/p/g3d/code/HEAD/tree/G3D10/data-files/shader/MotionBlur/MotionBlur_gather.pix

auto f = zCompare(z_p, z_S);
auto b = zCompare(z_S, z_p);

auto weight = 0.0f;
weight += b * cone(T, 1 / mag_vs);
weight += f * cone(T, 1 / mag_vc);
weight += cylinder(T, min(mag_vs, mag_vc)) * 2.0f;

totalWeight += weight;
result += colorSample * weight;

なねぃと調査

こんばんわ。Pocolです。
もうすぐSIGGRAPHですね。SIGGRAPHのコースで「なねぃと」についての話があるそうで,そのコースを受ける前にある程度は調査しておこうと思いました。Sさん、問題あればご連絡を。

個人的に知りたいこと

今回の調査では下記のようなことを知りたいなと思ったので,調査してみました。

  • どんなレンダリングフローなのか?
  • 実際にどんなシェーダ使っているの?
  • どうやってストリーミングするものを決めてるの?
  • ストリーミングデータはどうやって作るのか?

大雑把な流れ

なねぃとは仮想化マイクロポリゴンジオメトリシステムです。いわゆるVirtual Textureみたいなテクスチャストリーミングのメッシュ版という感じのやつです。細かいポリゴンを扱えるのが売りになっていて,ものすごくいい感じのディテールが表現できます。
なねぃとを実現するためのキーだと思っているものは次の通りです。

  • GPU駆動描画
  • 高速なソフトウェアラスタライズ
  • Deferred Material(Visibility Buffer)
  • トライアングルデータの圧縮
  • 階層LODの構築

まずGPU駆動描画はその名の通り,GPU上で描画するかどうかを判定を行い,その結果で描画が駆動する手法のことを言ったりします。これはカプコンさんだったり,アサクリだったり,Trialsだったりと色々な会社さんがすでに取り組まれています。内容について知らない方がいたら下記の資料などを読むと良いと思います。

UE5はバウンディングのスケールに応じてソフトウェアラスタライズとハードウェアラスタライズの分岐がコンピュートシェーダ上で決定されます。
小さな三角形はコンピュートシェーダを用いたソフトウェアラスタライズが実行され,大きな三角形に対してハードウェアラスタライザが実行されます。
ソフトウェアラスタライザですが,小さな三角形に対しては平均で3倍高速化したと「Nanite | Inside Unreal」の動画でKarisが言っていました。恐るべき速度ですね。

※図は”Nainte | Inside Unreal”より引用

あとは,この高速なソフトウェアラスタライズを支える技術として,Deferred Materialを使用しています。いわゆるVisibility Bufferというやつです。
これの何が良いかというと,深度とマテリアルインデックスをバッファに出力してしまい,マテリアル評価を遅延実行できるというメリットがあります。つまり,いちいちシェーダの切り替えをしなくて良いということです。これによりUE5は不透明物体の描画を1ドローで実現しています。

※図は”Nainte | Inside Unreal”より引用

さらにNaniteを見ていてがんばっているなーと思うのがデータの圧縮です。1トライアングルにつき平均14.4Byteだそうです。一応自分でもソースコードを追ってみて,確かにそうだなということを確認しました。

※図は”Nainte | Inside Unreal”より引用

どんなレンダリングフローなのか?

まずは,レンダラーの流れをつかみ大雑把にどんなことをやっているのかを把握してみました。
レンダラーの実装は,Engine/Source/Runtime/Renderer/Private/DeferredShadingRenderer.cppにあります。これがディファードレンダリングの実装になっており,Render()メソッドに実装があるので,これを地味に読み解いていきました。なんとこの関数1600行ほどあります。

この関数をところどころを端折った疑似コードが下記のような感じになります。

ざっくりですが,処理をまとめておくと

  • グローバルリソースを更新
  • 非同期でファイル読み込みを開始
  • 読み込みタスクの完了待ちをして,GPUにデータを転送
  • Visibility Bufferをクリア
  • ビュー情報をパッキングしてカリングとラスタライズを実行
  • 深度バッファに出力
  • マテリアルごとにドローコールを発行して,タイル描画を行うことによりG-Bufferを構築

…という感じです。

実際にどんなシェーダ使っているの?

CullRasterize()

下記のシェーダが実行されるようです。

  • InstanceCull :パス Engine/Private/Nanite/InstanceCulling.usf
  • InitArgs :パス Engine/Private/Nanite/ClusterCulling.usf
  • InstanceCullVSM :パス Engine/Private/Nanite/InstanceCulling.usf
  • PersistentClusterCull : パス Engine/Private/Nanite/ClusterCulling.usf
  • CalculateSafeRasterizerArgs : パス Engine/Private/Nanite/ClusterCulling.usf
  • HWRasterizerVS : パス Engine/Private/Nanite/Rasterizer.usf
  • HWRasterizerPS : パス Engine/Private/Nanite/Rasterizer.usf
  • MicropolyRasterize : パス Engine/Private/Nanite/Rasterizer.usf

左側は実行される関数で,右側はその関数が実装されているファイルパスを表します。
PersistentCull()がおそらく一番巨大なシェーダコードになると思うのですが,クラスタ階層のトラバーサルとかカリング,あとはソフトウェアラスタライズかハードウェアラスタライズかどうかの切り分けなんかも行っています。
MicroPolyRasterize()がソフトウェアラスタライズを行っているのですが,R32G32のバッファに書き込みをします。Rチャンネルの25bit分がVisibleIndexで,残りの7bitがTriangleIDになっています。GチャンネルはDepthに割り当てられているようです。

EmitDepthTargets()

下記のシェーダが実行されるようです。

  • DepthExport : パス Engine/Private/Nanite/DepthExport.usf
  • EmitSceneDepthStencilPS : パス Engine/Private/Nanite/ExportGBuffer.usf
  • EmitSceneSStencilPS : パス Engine/Private/Nanite/ExportGBuffer.usf

名前通りの処理っぽいです。

DrawBasePass()

下記のシェーダが実行されるようです。

  • ClassifyMaterials : パス Engine/Private/Nanite/MaterialCulling.usf
  • FullScreenVS : パス Engine/Private/Nanite/ExportGBuffer.usf

ClassifyMaterials()の中では,VisibleなMaterialとMaterial Rangeを決定するようです。Material Rangeにはマテリアルのマスクビットが格納されています。
大雑把な理解として,描画対象となるマテリアルの矩形範囲を決めているようです。
FullScreenVS()では,決定したマテリアルの矩形範囲にあるタイルで,本当にそのマテリアルの描画必要かどうかを判定して,要らないところはNaNを頂点シェーダで設定して,タイルをカリングするという処理を行うようです。タイルカリングには5つのモードがあるようです。

ストリーミングの仕組みは?

ストリーミングの管理はFStreamingManagerというクラスで管理されているようです。
Engine/Source/Runtime/Engine/Public/Rendering/NaniteStreamingManager.hにクラスの宣言があります。
・クラスタページデータ
・クラスタページヘッダ
・クラスタ修正更新バッファ
・ストリーミングリクエストバッファ
・ストリーミングリクエストリードバックバッファ
・ペンディングページ
・リクエストハッシュテーブル
などを保持しているようですが,まだ理解しきれていないので,理解できるようになったら別の記事として書くことにします。
ページリクエストは,PersistentClusterCull()というシェーダが実行されてRequestPageRange()という関数内でリクエストが書き込まれるようです。
このシェーダで書き込んだページリクエストの取得はディファードレンダラー内のAsyncUpdate()内でバッファをmapすることでCPU側で取得されるようです。
リクエストハッシュテーブルにGPUから取得したものを登録して,被るものがあるかどうかをチェック。チェックにヒットしたらストリーミングするページとしてプッシュし,優先度でソートして,LRUを更新しています。
一方,検索にヒットしない場合は,優先度付きリクエストヒープにいったんプッシュするようです。その後、このヒープからポップしてストリーミングするページを選択しています。
ストリーミングするものは読み込みされている状態なので,送ればいいのですが,これから読み込みしないといけないペンディング状態のものについては,ペンディング状態のページを収集し,ランタイムリソースIDがコンピュートシェーダで書き込まれるので,これを利用してFbyteBulkDataを取得するようです。取得したデータをFIORequestTaskに登録し,ParallelFor文を用いて,ファイルの非同期読み込みが実行さるようです。
ストリーミングマネージャのEndAsyncUpdate()という関数で,読み込み完了待ちをしてからResourceUplodTo()関数を使ってGPUに転送しているみたいです。

ストリーミングデータはどうやってつくるのか?

メッシュデータの構築

FStaticMeshBuilder::Build()経由でBuildNaniteFromHiResourceModel()が呼び出されて,データが作成されるようです。
BuildDAG(), ReduceDAG(), FindDAGCut()などの内部の処理がまだ全然理解できていないので,鋭意調査中です。

エンコード

ジオメトリデータはEncodeGeometryData()という関数でエンコードされるようです。
Positionデータは63bitに。
法線ベクトルはOctrahedronで表現し,XYで18bitに。
テクスチャ座標はXYで32bitに。
ここまで合計14byteになるので,確かにKarisが言っていた平均14.4Bという数字は納得できます。
ちなみに頂点カラーやUV数を増やす場合はさらにデータ容量が増えていく感じです。

おわりに

今回は,えらくざっくりですが,どんな感じで動くのかについて調査しました。
レンダリングフローについては理解できたのですが,実際のストリーミングの詳しい処理内容や,階層LODの作成方法など,まだまだわからない箇所もあるので次回調査してみたいと思います。

順調です。

こんばんわ、Pocolです。
アレですが,順調に進んでおります。機が熟したら書きますので、もう少しお待ちください。

資料:THE LAST OF US PARTⅡ

実装用の資料として,THE LAST OF US PARTⅡのスクショを張っておきます。動画も上げようと思ったのですが,サイズが大きくて上げられませんでした。

The Last of Us® Part II_20210124181203
The Last of Us® Part II_20210124181223
The Last of Us® Part II_20210124181237
The Last of Us® Part II_20210124181253
The Last of Us® Part II_20210124181308
The Last of Us® Part II_20210124181326
The Last of Us® Part II_20210124181344
The Last of Us® Part II_20210124181413
The Last of Us® Part II_20210124181429
The Last of Us® Part II_20210124181444
The Last of Us® Part II_20210324214828
The Last of Us® Part II_20210324214847
The Last of Us® Part II_20210324214923
The Last of Us® Part II_20210324215312
The Last of Us® Part II_20210324215326
The Last of Us® Part II_20210324215343
The Last of Us® Part II_20210324215354
The Last of Us® Part II_20210324215403
The Last of Us® Part II_20210324215601
The Last of Us® Part II_20210324215609
The Last of Us® Part II_20210324215620
The Last of Us® Part II_20210324215919
The Last of Us® Part II_20210324215930
The Last of Us® Part II_20210324215941