待たせたな!(スネーク風)
(さらに…)
私的メモ:モーションブラー
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だったりと色々な会社さんがすでに取り組まれています。内容について知らない方がいたら下記の資料などを読むと良いと思います。
- 三嶋仁, 清水昭尋, “GPU駆動レンダリングへの取り組み”, Game Creaters Conference 2016
- Ulrich Harr, Sebastian Aaltonen, “GPU-Driven Rendering Pipelines”, SIGGRAPH 2015: Advances in Real-Time Rendering in Games
- Graham Wihlidal, “Optimizing the Graphics Pipeline with Compute”, GDC 2016
- Olksandr Drazhevskyi, “GPU Driven Rendering and Virtual Texturing in Trials Rising
UE5はバウンディングのスケールに応じてソフトウェアラスタライズとハードウェアラスタライズの分岐がコンピュートシェーダ上で決定されます。
小さな三角形はコンピュートシェーダを用いたソフトウェアラスタライズが実行され,大きな三角形に対してハードウェアラスタライザが実行されます。
ソフトウェアラスタライザですが,小さな三角形に対しては平均で3倍高速化したと「Nanite | Inside Unreal」の動画でKarisが言っていました。恐るべき速度ですね。
あとは,この高速なソフトウェアラスタライズを支える技術として,Deferred Materialを使用しています。いわゆるVisibility Bufferというやつです。
これの何が良いかというと,深度とマテリアルインデックスをバッファに出力してしまい,マテリアル評価を遅延実行できるというメリットがあります。つまり,いちいちシェーダの切り替えをしなくて良いということです。これによりUE5は不透明物体の描画を1ドローで実現しています。
さらにNaniteを見ていてがんばっているなーと思うのがデータの圧縮です。1トライアングルにつき平均14.4Byteだそうです。一応自分でもソースコードを追ってみて,確かにそうだなということを確認しました。
どんなレンダリングフローなのか?
まずは,レンダラーの流れをつかみ大雑把にどんなことをやっているのかを把握してみました。
レンダラーの実装は,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Ⅱのスクショを張っておきます。動画も上げようと思ったのですが,サイズが大きくて上げられませんでした。
もうすこし待たれよ!
こんにちわ、Pocolです。
執筆している本ですが…,順調に進んでいます。
企画から5年。そして無名ということで企画が通るまでの,半年近くはほぼ何もできず。
担当さんの地道な根回しがあって,ようやく執筆までたどり着いた本です。
折角企画も通り,発売に向けて動き出したので…
発売後にマサカリ投げられるのは,嫌なので説明部分に関してはあらかじめ豪華メンバーに査読していただきました。
説明については丁寧にレビューしていただきました。
本当にこの場を借りて,感謝を述べさせていただきたいと思います。
「本業がありタイトル開発などお忙しい中,レビューアーの皆様本当にありがとうございました!」
実をいうと,レビューで心が折れました。
やっぱり,色々と見てもらうと自分がいかに愚か者であるかということをヒシヒシと感じますね。
説明の仕方もそうですが,言葉の選び方や言い回しなんかもそうです。
独りよがりはやっぱりいかんなと改めて思いました。
また、レビューしていただいて本当によかったなと心の底から感じています(本当最初の状態は酷かった)。
レビュー無しで,国会図書館なんかに寄贈されて,ダメダメな説明とかが自分が死んだ後も一生記録されてしまうと思うと,ゾッとしますね。
レビューが無かったら,そうした恥をさらしながら生きていかなければならないのですが,レビューをしてもらうことで,少しでも回避することができて本当によかったなと思います。
やはり,レビューをしてもらって感じたのですが,きちんとした議論があってこそ,良いものが生まれてくるんだろうなと… そう感じました。
自分が見ていて「これは良いな!」と思う書籍は共著だったり,きちんと監修が付いていたりしますしね。
自分は筆不精でして,期待されて待っている方には本当に申し訳ないのですが,
実はとある競合書籍が発売されていなければ,今頃は既に発売済みだったんですね。
何もせずに,当初通りそのまま発売というのもありだったのですが,単なる2番煎じになるもの,流石にどうかなと思いまして…。
(まぁ、だったら先越される前に早く書けよっていうのはごもっともです。)
やはり,先手を打ち出されてしまうと,後手側は何かしらの対策を練らねければなりません。
何も対策を練らないというのは愚の骨頂ではないかと思いましたし,何かしら付加価値を付けて提供したいと思いました。
また,何もせずにそのまま発売にもっていってしまうのは,色々な方の力を借りている分,自分として申し訳なかったですし,待っている方にも申し訳が立たないです。
担当さんは,「こちらのほうが先にやっているのに…後からを先越されて…」と,がっくし来ていたみたいです。
自分もさらに,その本の内容を見てガックシきました。ちなみに,その本の内容は,細かい所は違えど目指す方向性としては,自分が一番最初に出版社に企画を出してボツになった内容ほぼそのものでした。こっちの出版社だったら,すんなり受け入れられていたかもしれないな…って。
まぁ、いずれこういう先に越されることは目に見えていたので,筆不精な自分が悪い以外のなにものでもないんですけどもね。
ただ,今書いている内容はボツになったものとは方向性が違います。
…というか,出版社側のダメ出しだったり,他社に先越されるのとか想定して,じゃぁちょっと変えましょうと,企画段階で変更したんですよね。
だから,そのまま予定に沿って発売に向けて動いても問題はなかったのですが,何か悔しいなと思って…。
何かしら手は打たねばならないと思いました。
そこで,1章分を新たに追加執筆し,さらに開発に役立つと思われる付録もちょっとしょぼい内容ですが一応付けました。
具体的な内容については,もう少し言える状況になったらお伝えしたいと思います。
(内容を書いて競合他社にパクられて,差別化をするためにまた発売を延期するのは流石に勘弁したいので…)
皆さん,完成まで着々と進んでいます。もうしばらくお待ちください!!
明けましておめでとうございます。
新年あけましておめでとうございます!
本年度もどうぞProjectASURAをよろしくお願い致します。
今年は執筆していた本も発売されるので,本発売まで更新停止していたホームページもぼちぼち更新頻度を高められたらなぁと思っています。
色々とノウハウも徐々についてきたので,どこかで文章化出来たらいいなと思っています。
あとは昨年から息抜きとしてゲームっぽいものも作り始めました。こちらについてもある程度形になってきたらホームページの方で記事化出来たらと思ってます。
動画にしてみた。 pic.twitter.com/SzYmJVhAis
— Pocol (@ProjectAsura) November 23, 2020
簡易な敵を作って,攻撃判定も付けてみた。 pic.twitter.com/pQjtqnnFuW
— Pocol (@ProjectAsura) November 23, 2020
雑だけどHPゲージ付けた。 pic.twitter.com/k0kFXMV62k
— Pocol (@ProjectAsura) November 24, 2020
押せるギミックを実装してみた。 pic.twitter.com/1GzCzM3Eh0
— Pocol (@ProjectAsura) November 26, 2020
キャラが微妙にずれているけど,一応マップ遷移の仕組み作った。(マップ作るのがめんどいので,同じレイアウトのままですが…) pic.twitter.com/tfCDgW1vpx
— Pocol (@ProjectAsura) November 29, 2020
Dual Senseのサポート始めてみました。
おはようございます。Pocolです。
こちらで改造版RdpGamepadですが,DualSenseを試験的にサポートしてみることにしました。
Dual Sense Alpha Version
基本的には,開発しているlibDS4を差し替えただけなので,RdpGamepad側のロジック変更はありません。
人柱になってくれるかたがいらっしゃいましたら,不具合報告などをいただけると有難いです。
また,ViGEmBus側が,Dual Senseに対応していないので,転送先PCではDS4かXBoxコントローラーとしてしか振舞いませんので注意してください。
キューブマップからスフィアマップへの変換
おはござ!Pocolです。
前回の記事では,スフィアマップのサンプルについて紹介しましたが,今回はキューブマップをスフィアマップ形式で展開して表示する方法について紹介します。
キューブマップからの変換については,Paul Bourke氏の“Converting to/from cubemaps”にまとまった説明があります。
この記事に書かれているキューブマップから正距円筒方式への変換処理は次のような感じになります。
// スフィアマップ形式で表示するためのキューブマップ参照方向を求めます. float3 ToCubeMapCoord(float2 texcoord) { // [-1, 1]に変更. float2 uv = texcoord * float2(2.0f, -2.0f) - float2(1.0f, -1.0f); float theta = uv.x * F_PI; float phi = uv.y * F_PI * 0.5f; float3 dir; dir.x = cos(phi) * cos(theta); dir.y = sin(phi); dir.z = cos(phi) * sin(theta); return dir; }
この変換の使いどころですが,ImGuiでキューブマップを表示するのが面倒なので,上記の関数をかまして2Dマップとして表示するのに自分は使用しています。
…というわけで,展開して表示する方法について紹介しました。