Slide 1

Slide 1 text

No content

Slide 2

Slide 2 text

• 本チュートリアルで説明する内容は, 予めご注意ください。 • 間違い等があれば,修正例と共にご指摘頂けると幸いです。 2

Slide 3

Slide 3 text

なぜ、このチュートリアルをやろうと思ったのか? 3

Slide 4

Slide 4 text

4

Slide 5

Slide 5 text

5

Slide 6

Slide 6 text

• 年々 • ゲームのグラフィックスは進歩しています • 年々ゲームの制作物量も増えつつあります • 人は常に足りない • 優秀な人財確保・戦力確保は各社の共通の課題 • 特に、グラフィックスプログラマーに関しては不足していて,中々雇えない、補充できない 6

Slide 7

Slide 7 text

• GPU駆動描画,UE5 Naniteやメッシュシェーダの登場によりメッシュ描画は現在大変革期! [Wihlidal 2016][Uralsky 2019][Karis 2021] • メッシュレット描画を採用するゲームもチラホラ出てきた。 [Jansson 2024][Lopez 2025][Mishima 2025] 7

Slide 8

Slide 8 text

近年注目が集まっている以下の3点に焦点を当てた 技術紹介および解説を行います。 8

Slide 9

Slide 9 text

• • • • • • • 9

Slide 10

Slide 10 text

• • • • • • • 10

Slide 11

Slide 11 text

基本事項のおさらいから… 11

Slide 12

Slide 12 text

12

Slide 13

Slide 13 text

13 B個 A個 C個 X個 Y個 Z個 ID3D12GraphicsCommandList::Dispatch(A, B, C) でコンピュートシェーダを起動。 [numthreads(X, Y, Z)] void main(…) { … }

Slide 14

Slide 14 text

14 Wave Active Lane Inactive Lane Wave 一定数のスレッドで構成される最小の処理単位 Waveの数は4, 8, 16, 32, 64, 128 のいずれか[Microsoft 2021] Lane Wave内の各スレッド Active Lane Wave内で命令を実行するスレッド Inactive Lane Wave内で命令を実行しないスレッド

Slide 15

Slide 15 text

• Wave内のすべてのLaneは、同じシェーダを同期して実行 • Wave内で実行ダイバージェントが発生する分岐を持つ場合は,両方の処理コストを払う必要がある 15

Slide 16

Slide 16 text

• Wave内でスレッドに依存して変わる値をベクトル値と呼びます。Wave内でスレッドに依存しない値をスカラー値と呼びます。 • ベクトル値は VGPR (Vector General Purpose Register) に格納される(Wave内で全部共通にならない値) スカラー値は SGPR (Scalar General Purpose Register) に格納される(Wave内で全部共通になる値) • スカラー値による分岐は実行ダイバージェンスを発生させないため,別条件の分岐処理も実行するという無駄が無くなる → ベクトル値による分岐に比べるとパフォーマンスペナルティは比較的に少なくなる → スカラー分岐になるように「スカラー化する」(Scalarization)という手法がある → スカラー化するテクニックの1つとして,Wave Intrinsicsを利用する手法がある 16

Slide 17

Slide 17 text

• Wave IntrinsicsはWave内のLane間でのデータの交換や演算を行うための組み込み関数で, 同期命令なしで,他のLaneの変数を参照したり,演算することが可能 • Wave Intrinsicsの各関数についての説明は, “HLSLのWave Intrinsicsについて“[shikihuiku 2020] が非常に参考になる • Wave Intrinsicsを用いた有用なテクニックについては ”Compute shader wave intrinsics tricks”[Sreckovic 2024]が参考になる 17 A B C D float4 sum = tex.Sample(smp, uv, int2(0, 0)) // A + tex.Sample(smp, uv, int2(0, 1)) // B + tex.Sample(smp, uv, int2(1, 0)) // C + tex.Sample(smp, uv, int2(1, 1)); // D float4 ave = sum * 0.25; float4 val = tex.Sample(smp, uv, int2(0, 0)); float3 sum = WaveReadLaneAt(val, 5) // A + WaveReadLaneAt(val, 6) // B + WaveReadLaneAt(val, 7) // C + waveReadLaneAt(val, 8) // D float4 ave = sum * 0.25; LaneId=5 LaneId=6 LaneId=7 LaneId=8

Slide 18

Slide 18 text

• • • • • • • 18

Slide 19

Slide 19 text

• • • • • • • 19

Slide 20

Slide 20 text

新しいグラフィックスパイプライン 20

Slide 21

Slide 21 text

21 スッキリ! 従来パイプライン 新しいパイプライン

Slide 22

Slide 22 text

22 メッシュシェーダの起動の増幅を行える。 これによって,ジオメトリシェーダが担って いたポリゴンの増減やテッセレータの代替え を行うことができる。 増幅シェーダの使用は任意。 使用しなくてもいい。

Slide 23

Slide 23 text

23 struct PayloadData { uint MeshletIndices[32]; }; groupshared PayloadData s_Payload; [numthreads(32, 1, 1)] void main ( uint gtId : SV_GroupThreadId, uint dtId : SV_DispatchThreadId, uint groupId : SV_GroupID ) { bool visible = false; if (dtId < MeshInfo.MeshletCount) { visible = IsVisible(CullingData[dtId], MeshInstance); } if (visible) { uint index = WavePrefixCountBits(visible); s_Payload.MeshletIndices[index] = dtId; } uint visibleCount = WaveActiveCountBits(visible); DispatchMesh(visibleCount, 1, 1, s_Payload); }

Slide 24

Slide 24 text

24 入力アセンブラ(IA)に依存せずに一定単位 で頂点処理を行える。 頂点シェーダよりも GPUフレンドリーな並列処理が強い。 仕様上の制限として 最大256プリミティブ(最大256頂点) までしか扱えない。

Slide 25

Slide 25 text

• メッシュシェーダは,仕様上で最大256ポリゴンしか一度に出力できない。 多数のポリゴンから構成されるメッシュを描画する場合は, 256ポリゴン単位などに細かく分割し,複数回メッシュシェーダを起動する必要がある。 25

Slide 26

Slide 26 text

26 [outputtopology(“triangle”)] [numthreads(12, 1, 1)] void main ( in uint groupThreadId : SV_GroupThreadID, out vertices MSOutput vertices[8], out indices uint3 indices [12] ) { SetMeshOutputCounts(8, 12); if (groupThreadId < 8) { float4 localPos = InputVertices[groupThreadId]; float4 projPos = mul(ViewProjMatrix, localPos); vertices[groupThreadId].Position = projPos; vertices[groupThreadId].Color = InputColors[groupThreadId]; } indices[groupThreadId] = InputIndices[groupThreadId]; }

Slide 27

Slide 27 text

• メリット • コンピュートシェーダ的に動作するので,並列動作に強い。 • 柔軟性があり,GPU内で可視性・LOD評価などがしやすい。 • ジオメトリ生成パフォーマンスが向上する。 • デメリット • 古いGPUで使用不可。 • デバッグが難しい あるいは 面倒くさい。 • メッシュアセットの生成コンバーターを変える必要がある。 27

Slide 28

Slide 28 text

• 従来ジオメトリシェーダや,ハルシェーダ、ドメインシェーダなどで処理していたものは メッシュシェーダと増幅シェーダを利用することで実装可能 (例)メッシュシェーダを利用したサブディビジョンサーフェイス[Akuzawa 2024] 29

Slide 29

Slide 29 text

実装事例を交えた使用例の解説 30

Slide 30

Slide 30 text

31

Slide 31

Slide 31 text

32

Slide 32

Slide 32 text

前述したようにメッシュシェーダは 最大256ポリゴンしか扱うことができない メッシュを細かく分割したメッシュレットを作成する必要がある “Performance Comparison of Meshlet Generation Strategies” [Jensen 2023] • 各生成手法のパフォーマンス比較した論 • 論文に書いてある中から3つの手法を紹介 • • • 33

Slide 33

Slide 33 text

• Bounding Boxの最長軸でソートした頂点リストと三角形リストを用意 • 頂点を1つピックアップする。その頂点を含む三角形について調査 • 三角形がメッシュレットに未登録なら3頂点を調べる。登録済みなら次の三角形へ • ピックアップした頂点が未登録なら,登録する。登録済みなら次の頂点へ • メッシュレットが満杯でなければ,先ほど調査した三角形をメッシュレットに追加 34

Slide 34

Slide 34 text

• Bounding Boxの最長軸でソートした頂点リストと三角形リストを用意 • 頂点を1つピックアップする。その頂点を含む三角形について調査 • 三角形がメッシュレットに未登録なら3頂点を調べる。登録済みなら次の三角形へ • ピックアップした頂点が未登録なら,登録する。登録済みなら次の頂点へ • メッシュレットが満杯でなければ,先ほど調査した三角形をメッシュレットに追加 35

Slide 35

Slide 35 text

• Bounding Boxの最長軸でソートした頂点リストと三角形リストを用意 • 頂点を1つピックアップする。その頂点を含む三角形について調査 • 三角形がメッシュレットに未登録なら3頂点を調べる。登録済みなら次の三角形へ • ピックアップした頂点が未登録なら,登録する。登録済みなら次の頂点へ • メッシュレットが満杯でなければ,先ほど調査した三角形をメッシュレットに追加 36

Slide 36

Slide 36 text

• Bounding Boxの最長軸でソートした頂点リストと三角形リストを用意 • 頂点を1つピックアップする。その頂点を含む三角形について調査 • 三角形がメッシュレットに未登録なら3頂点を調べる。登録済みなら次の三角形へ • ピックアップした頂点が未登録なら,登録する。登録済みなら次の頂点へ • メッシュレットが満杯でなければ,先ほど調査した三角形をメッシュレットに追加 37

Slide 37

Slide 37 text

• Bounding Boxの最長軸でソートした頂点リストと三角形リストを用意 • 頂点を1つピックアップする。その頂点を含む三角形について調査 • 三角形がメッシュレットに未登録なら3頂点を調べる。登録済みなら次の三角形へ • ピックアップした頂点が未登録なら,登録する。登録済みなら次の頂点へ • メッシュレットが満杯でなければ,先ほど調査した三角形をメッシュレットに追加 38

Slide 38

Slide 38 text

• Bounding Boxの最長軸でソートした頂点リストと三角形リストを用意 • 頂点を1つピックアップする。その頂点を含む三角形について調査 • 三角形がメッシュレットに未登録なら3頂点を調べる。登録済みなら次の三角形へ • ピックアップした頂点が未登録なら,登録する。登録済みなら次の頂点へ • メッシュレットが満杯でなければ,先ほど調査した三角形をメッシュレットに追加 39

Slide 39

Slide 39 text

• Bounding Boxの最長軸でソートした頂点リストと三角形リストを用意 • 頂点を1つピックアップする。その頂点を含む三角形について調査 • 三角形がメッシュレットに未登録なら3頂点を調べる。登録済みなら次の三角形へ • ピックアップした頂点が未登録なら,登録する。登録済みなら次の頂点へ • メッシュレットが満杯でなければ,先ほど調査した三角形をメッシュレットに追加 40

Slide 40

Slide 40 text

• Bounding Boxの最長軸でソートした頂点リストと三角形リストを用意 • 頂点を1つピックアップする。その頂点を含む三角形について調査 • 三角形がメッシュレットに未登録なら3頂点を調べる。登録済みなら次の三角形へ • ピックアップした頂点が未登録なら,登録する。登録済みなら次の頂点へ • メッシュレットが満杯でなければ,先ほど調査した三角形をメッシュレットに追加 41

Slide 41

Slide 41 text

• Bounding Boxの最長軸でソートした頂点リストと三角形リストを用意 • 頂点を1つピックアップする。その頂点を含む三角形について調査 • 三角形がメッシュレットに未登録なら3頂点を調べる。登録済みなら次の三角形へ • ピックアップした頂点が未登録なら,登録する。登録済みなら次の頂点へ • メッシュレットが満杯でなければ,先ほど調査した三角形をメッシュレットに追加 42

Slide 42

Slide 42 text

• Bounding Boxの最長軸でソートした頂点リストと三角形リストを用意 • 頂点を1つピックアップする。その頂点を含む三角形について調査 • 三角形がメッシュレットに未登録なら3頂点を調べる。登録済みなら次の三角形へ • ピックアップした頂点が未登録なら,登録する。登録済みなら次の頂点へ • メッシュレットが満杯でなければ,先ほど調査した三角形をメッシュレットに追加 43

Slide 43

Slide 43 text

• Bounding Boxの最長軸でソートした頂点リストと三角形リストを用意 • 頂点を1つピックアップする。その頂点を含む三角形について調査 • 三角形がメッシュレットに未登録なら3頂点を調べる。登録済みなら次の三角形へ • ピックアップした頂点が未登録なら,登録する。登録済みなら次の頂点へ • メッシュレットが満杯でなければ,先ほど調査した三角形をメッシュレットに追加 44

Slide 44

Slide 44 text

45

Slide 45

Slide 45 text

• 貪欲法と似ているが,三角形の追加の仕方が異なる • 開始頂点から初めて,Bonding Sphereの半径が最小となるように三角形を追加 • 追加には[Baerentzen 2021]のアルゴリズムを利用 46

Slide 46

Slide 46 text

47

Slide 47

Slide 47 text

• 論文著者の実装コードが下記にある。MITライセンス。 • https://github.com/Senbyo/meshletmaker/tree/main • 貪欲法の実装は… core/meshletMeshDescriptor.cpp generateMeshlets() 1943行目~1998行目を参照 • Bounding Sphere法の実装は… core/meshletMeshDescriptor.cpp generateMeshlets() 493行目~690行目を参照 48

Slide 48

Slide 48 text

49 Greedy

Slide 49

Slide 49 text

50 Bounding Sphere法

Slide 50

Slide 50 text

• meshoptimizerというオープンソースライブラリを使う手法 https://github.com/zeux/meshoptimizer • 実装が容易であり,実行速度が高速。MITライセンス。 • 他社での採用事例あり (https://meshoptimizer.org/USERS.html) meshopt_buildMeshlets()というメソッドで生成を行う 51

Slide 51

Slide 51 text

52 const size_t max_vertices = 64; // 最大頂点数を定義. const size_t max_triangles = 124; // 最大プリミティブ数を定義. const float cone_weight = 0.0f; // メッシュレット領域を生成. size_t max_meshlets = meshopt_buildMeshletsBound(indices.size(), max_vertices, max_triangles); std::vector meshlets (max_meshlets); std::vector meshlet_vertices (max_meshlets * max_vertices); std::vector meshlet_triangles(max_meshlets * max_triangles * 3); // メッシュレットを生成. size_t meshlet_count = meshopt_buildMeshlets( meshlets.data(), meshlet_vertices.data(), meshlet_triangles.data(), indices.data(), indices.size(), &vertices[0].x, vertices.size(), sizeof(Vertex), max_vertices, max_triangles, cone_weight);

Slide 52

Slide 52 text

53

Slide 53

Slide 53 text

54

Slide 54

Slide 54 text

55

Slide 55

Slide 55 text

56

Slide 56

Slide 56 text

57 [outputtopology(“triangle”)] [numthreads(12, 1, 1)] void main ( in uint groupThreadId : SV_GroupThreadID, out vertices MSOutput vertices[8], out indices uint3 indices [12] ) { SetMeshOutputCounts(8, 12); if (groupThreadId < 8) { float4 localPos = InputVertices[groupThreadId]; float4 projPos = mul(ViewProjMatrix, localPos); vertices[groupThreadId].Position = projPos; vertices[groupThreadId].Color = InputColors[groupThreadId]; } indices[groupThreadId] = InputIndices[groupThreadId]; }

Slide 57

Slide 57 text

58 [outputtopology(“triangle”)] [numthreads(12, 1, 1)] void main ( in uint groupThreadId : SV_GroupThreadID, out vertices MSOutput vertices[8], out indices uint3 indices [12] ) { SetMeshOutputCounts(8, 12); if (groupThreadId < 8) { float4 localPos = InputVertices[groupThreadId]; float4 projPos = mul(ViewProjMatrix, localPos); vertices[groupThreadId].Position = projPos; vertices[groupThreadId].Color = InputColors[groupThreadId]; } indices[groupThreadId] = InputIndices[groupThreadId]; }

Slide 58

Slide 58 text

59 [outputtopology(“triangle”)] [numthreads(12, 1, 1)] void main ( in uint groupThreadId : SV_GroupThreadID, out vertices MSOutput vertices[8], out indices uint3 indices [12] ) { SetMeshOutputCounts(8, 12); if (groupThreadId < 8) { float4 localPos = InputVertices[groupThreadId]; float4 projPos = mul(ViewProjMatrix, localPos); vertices[groupThreadId].Position = projPos; vertices[groupThreadId].Color = InputColors[groupThreadId]; } indices[groupThreadId] = InputIndices[groupThreadId]; }

Slide 59

Slide 59 text

• 入力システムセマンティクス値として次の値が利用可能。 • uint3 SV_DispatchThreadID • uint3 SV_GroupThreadID • uint SV_GroupIndex • uint3 SV_GroupID • uint SV_ViewID • 出力システムセマンティクス値として次の値が利用可能。 • bool SV_CullPrimitive 60

Slide 60

Slide 60 text

61 [outputtopology(“triangle”)] [numthreads(12, 1, 1)] void main ( in uint groupThreadId : SV_GroupThreadID, out vertices MSOutput vertices[8], out indices uint3 indices [12] ) { SetMeshOutputCounts(8, 12); if (groupThreadId < 8) { float4 localPos = InputVertices[groupThreadId]; float4 projPos = mul(ViewProjMatrix, localPos); vertices[groupThreadId].Position = projPos; vertices[groupThreadId].Color = InputColors[groupThreadId]; } indices[groupThreadId] = InputIndices[groupThreadId]; }

Slide 61

Slide 61 text

• メッシュの出力数を設定します。 • 制約 • この関数はシェーダごとに1回だけ呼び出し可能 • 呼び出しは共有出力配列への書き込みが行われる前に実行しないといけない • この関数を呼び出さない場合,ラスタライズ処理は実行されない • 最初のアクティブスレッドからの入力値のみが使用される • この関数の呼び出しを先に実行せずに, 出力配列への書き込みに到達する実行パスがあってはならない 62

Slide 62

Slide 62 text

63 if (uniform_cond) { SetMeshOutputCounts(…); } if (uniform_cond) { verts[…] = …; } if (divergent_cond) { SetMeshOutputCounts(…); } verts[…] = …; if (uniform_cond) { SetMeshOutputCounts(…); } else { SetMeshOutputCounts(…); } verts[…] = …;

Slide 63

Slide 63 text

64 [outputtopology(“triangle”)] [numthreads(12, 1, 1)] void main ( in uint groupThreadId : SV_GroupThreadID, out vertices MSOutput vertices[8], out indices uint3 indices [12] ) { SetMeshOutputCounts(8, 12); if (groupThreadId < 8) { float4 localPos = InputVertices[groupThreadId]; float4 projPos = mul(ViewProjMatrix, localPos); vertices[groupThreadId].Position = projPos; vertices[groupThreadId].Color = InputColors[groupThreadId]; } indices[groupThreadId] = InputIndices[groupThreadId]; }

Slide 64

Slide 64 text

• グループ全体で共有出力される配列 • 各配列は読み取り不可能 • インデックスは要素を一度に書き込む必要がある あとから他の要素を書き込むことは出来ない • 配列としては次の3種類がある 65 indices vertices primitives

Slide 65

Slide 65 text

66

Slide 66

Slide 66 text

67

Slide 67

Slide 67 text

• 次の2つの条件を要チェック。 • シェーダモデル6.5がサポートされていること。 • D3D12_MESH_SHADER_TIER_1以上がサポートされていること。 68 bool supportsSM6_5 = false; D3D12_FEATURE_DATA_SHADER_MODEL shaderModel = { D3D_SHADER_MODEL_6_5 }; auto hr = pDevice->CheckFeatureSupport(D3D12_FEATURE_SHADER_MODEL, &shaderModel, sizeof(shaderModel)); if (SUCEEDED(hr)) supportSM6_5 = (shaderModel.HighestShaderModel >= D3D_SHADER_MODEL_6_5); bool supportMS = false; D3D12_FEATURE_DATA_D3D12_OPTIONS7 features = {}; auto hr = pDevice->CheckFeatureSupport(D3D12_FEATURE_D3D12_OPTIONS7, &features, sizeof(features)); if (SUCEEDED(hr)) supportMS = (features.MeshShaderTier >= D3D12_MESH_SHADER_TIER_1);

Slide 68

Slide 68 text

• ID3D12Device2::CreatePipelineState()を使って生成する。 • 入力アセンブラ(IA)とStreamOutを無効化する必要がある。 • メッシュシェーダの指定は必須。 69 struct PSO_STREAM { CD3DX12_PIPELINE_STATE_STREAM_ROOT_SIGNATURE pRootSignature; CD3DX12_PIPELINE_STATE_STREAM_AS AS; // 増幅シェーダ. CD3DX12_PIPELINE_STATE_STREAM_MS MS; // メッシュシェーダ. … } stream; stream.AS = GetASByteCode(); stream.MS = GetMSByteCode(); … D3D12_PIPELINE_STATE_STREAM_DESC streamDesc = {}; streamDesc.pPipelineStateSubobjectStream = &stream; streamDesc.SizeInBytes = sizeof(stream); ID3D12PipelineState* pPso = nullptr; auto hr = pDevice->CreatePipelineState(&streamDesc, IID_PPV_ARGS(&pPso));

Slide 69

Slide 69 text

• メッシュシェーダおよび増幅シェーダの起動には, ID3D12GraphicsCommandList6::DispatchMesh() を利用する。 • DispatchMesh()の引数をそれぞれ a, b, cとした場合 • 3つのスレッドグループの数はそれぞれ64K未満でなければならない。 • a * b * c の積が 222(=4194304) を超えてはならない。 • DispatchMesh()をExecuteIndirect()で呼び出す場合は,以下を使用して設定する。 • D3D12_INDIRECT_ARGUMENT_TYPE_DISPATCH_MESH • D3D12_DISPATCH_MESH_ARGUMENTS 70

Slide 70

Slide 70 text

71 auto meshletCount = uint32_t(m_Meshlets.size()); pCmd->ClearRenderTargetView(handleRTV, m_ClearColor, 0, nullptr); pCmd->ClearDepthStencilView(handleDSV, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr); pCmd->OMSetRenderTargets(1, &handleRTV, FALSE, &handleDSV); pCmd->RSSetViewports (1, &m_Viewport); pCmd->RSSetScissorRects(1, &m_ScissorRect); pCmd->SetGraphicsRootSignature(m_RootSignature); pCmd->SetPipelineState(m_PipelineState); pCmd->SetGraphicsRoot32BitConstants (ROOT_B0, 1, &meshletCount, 0); pCmd->SetGraphicsRootConstantBufferView(ROOT_B1, m_MatrixCB.GetGPUVirtualAddress()); pCmd->SetGraphicsRootDescriptorTable (ROOT_T0, m_PosVB .GetHandleGPU()); pCmd->SetGraphicsRootDescriptorTable (ROOT_T1, m_NorVB .GetHandleGPU()); pCmd->SetGraphicsRootDescriptorTable (ROOT_T2, m_TexVB .GetHandleGPU()); pCmd->SetGraphicsRootDescriptorTable (ROOT_T3, m_PrimVB .GetHandleGPU()); pCmd->SetGraphicsRootDescriptorTable (ROOT_T4, m_VertIB .GetHandleGPU()); pCmd->SetGraphicsRootDescriptorTable (ROOT_T5, m_Meshlets.GetHandleGPU()); pCmd->DispatchMesh(meshletCount, 1, 1);

Slide 71

Slide 71 text

72

Slide 72

Slide 72 text

73

Slide 73

Slide 73 text

74

Slide 74

Slide 74 text

• • • • • • • 75

Slide 75

Slide 75 text

• • • • • • • 76

Slide 76

Slide 76 text

メッシュレットのLevel Of Detail 77

Slide 77

Slide 77 text

78

Slide 78

Slide 78 text

• マイクロポリゴン描画システムを導入する動きが増えつつある…。 79

Slide 79

Slide 79 text

• 4K解像度,8K解像度で絵作りをするためには密度感が大事 • 密度感を高めるには,ポリゴンが多く必要 • しかし,ポリゴンを多くすると処理負荷もメモリも増加 • 同じ問題を抱えるのがテクスチャ。 • Q. テクスチャの場合は? A. テクスチャストリーミングで解決を図る事例が多い。 80

Slide 80

Slide 80 text

• 仮想化ジオメトリシステム = Virtual Textureのジオメトリバージョン • 巨大なジオメトリを直接すべてメモリに展開することなく,小さな単位に分割して必要な分だけを GPUに転送して,描画する仕組み • 超高密度なメッシュがすべてメモリ上に存在しているかのように見せかけるという意味で 「仮想」という言葉がついている。 • これを実現するためのキーとなる技術が次の2つ。 81

Slide 81

Slide 81 text

82

Slide 82

Slide 82 text

83

Slide 83

Slide 83 text

84

Slide 84

Slide 84 text

• 次の手順で,多段階LODメッシュレットを生成していく。 1. メッシュからメッシュレットを生成する。 2. メッシュレットの接続関係を求める。接続関係に基づいてメッシュレットをグループ化する。 3. グループ個別に,インデックスバッファをマージして単純化(減ポリ)をする。 この際に,境界エッジを固定しマージ対象から外す。 4. 単純化されたグループを新たなメッシュレットに分割する。 5. 終了条件に達していなければ 2. に戻る。達していれば処理終了。 87

Slide 85

Slide 85 text

88 bool CreateLodMeshlets(const ResMeshlets& meshlets, ResLodMeshlets& lodMesh) { // サブセットごとにLODメッシュレットに変換. std::vector subsets; Conversion(meshlets, subsets); // サブセットのメモリを確保. lodMesh.Subsets.resize(subsets.size()); uint32_t maxLodLevel = 0; // マテリアルごとに処理. for(size_t i=0; i input = std::move(subsets[i].Meshlets); // LOD範囲. ResLodRange range = {}; range.Offset = uint32_t(lodMesh.Meshlets.size()); range.Count = uint32_t(input.size()); // マテリアルごとの全LODを含むメッシュレット総数のカウンター. uint32_t totalMeshletCount = uint32_t(input.size()); lodMesh.Subsets[i].MaterialId = subsets[i].MaterialId; lodMesh.Subsets[i].MeshletOffset = range.Offset; lodMesh.Subsets[i].LodRangeOffset = uint32_t(lodMesh.LodRanges.size()); lodMesh.LodRanges.emplace_back(range); // メッシュレットを追加. add_range(lodMesh.Meshlets, input); // LODレベル. uint32_t lodIndex = 1; // 指定数に達するまでループ. while(input.size() > 1 && lodIndex < (kMaxLodLevels - 1)) { …. } // 総数を記録. lodMesh.Subsets[i].MeshletCount = totalMeshletCount; lodMesh.Subsets[i].LodRangeCount = lodIndex; maxLodLevel = max(maxLodLevel, lodIndex); } lodMesh.Positions = meshlets.Positions; lodMesh.Normals = meshlets.Normals; lodMesh.Tangents = meshlets.Tangents; lodMesh.TexCoords = meshlets.TexCoords; lodMesh.BoundingSphere = meshlets.BoundingSphere; lodMesh.MaxLodLevel = maxLodLevel; lodMesh.LodRanges.shrink_to_fit(); // 正常終了. return true; } // 接続性に基づいてメッシュレットをグループ化. auto groups = GroupMeshlets(input); bool isMerged = false; std::vector simplifies; for(const auto& group : groups) { // グループ化したものを1つのメッシュにマージして,ポリゴン削減する. auto mergedInfo = SimplifyGroup(group, input, meshlets.Positions, meshlets.VertexIndices); // マージされていなければ以降の処理はスキップ. if (!mergedInfo.IsMerged) continue; float parentError = 0; for(auto& id : group.MeshletIds) { const auto& meshlet = input[id]; parentError = max(parentError, meshlet.GroupError); } // ポリゴン削減されたメッシュを,新しくメッシュレットに分割. auto newOnes = BuildMeshlets(mergedInfo, meshlets.Positions, lodIndex, subsets[i].MaterialId, parentError); const auto groupError = mergedInfo.Error + parentError; for(auto& id : group.MeshletIds) { // 1つ前のLOD(=入力データinput)が今新しく作ったメッシュレットの親になる. const auto offset = lodMesh.LodRanges.back().Offset; auto& parent = lodMesh.Meshlets[offset + id]; parent.ParentError = groupError; parent.ParentBounds = mergedInfo.BoundingSphere; } // 新しいメッシュレットを追加. add_range(simplifies, newOnes); // マージした. isMerged = true; } // 1回もマージされなければおしまい. if (!isMerged) break; // 新しいメッシュレットに差し替える. input = std::move(simplifies); // LOD範囲を設定. range.Offset = uint32_t(lodMesh.Meshlets.size()); range.Count = uint32_t(input.size()); lodMesh.LodRanges.emplace_back(range); totalMeshletCount += range.Count; // LODレベルをカウントアップ. lodIndex++; add_range(lodMesh.Meshlets, input);

Slide 86

Slide 86 text

• グループ単位で減ポリをするために,METIS[KarypisLab 2022]を使ってグループ分け • グループ分けの数(nparts)が多いと細かくなり,あまり減ポリされなくなる傾向があるので要注意。 89 Meshlet

Slide 87

Slide 87 text

90 idx_t constrainCount = 1; idx_t partsCount = count / kMinGroups; idx_t options[METIS_NOPTIONS] = {}; METIS_SetDefaultOptions(options); options[METIS_OPTION_OBJTYPE] = METIS_OBJTYPE_CUT; options[METIS_OPTION_CCORDER] = 1; // Identifies the connected components. options[METIS_OPTION_NUMBERING] = 0; // C-Style. idx_t edgeCut; // METISによって求められるカットの最終コスト. // METISを使ってグループ分けする. auto ret = METIS_PartGraphKway( &count, &constrainCount, xAdjacency.data(), edgeAdjacency.data(), nullptr, // vertex weights. nullptr, // vertex size edgeWeights.data(), &partsCount, nullptr, nullptr, options, &edgeCut, partition.data()); // グループ番号を記録する. std::vector groups(partsCount); for(auto i=0u; i{ group }; } idx_t count = idx_t(meshletCount); // idx_t はMETISで定義されている. std::vector partition; std::vector xAdjacency; std::vector edgeAdjacency; std::vector edgeWeights; partition .resize (count); xAdjacency.reserve(count + 1); for(size_t i=0; isecond; for(const auto& connectedMeshlet : connections) { // 自分自身ならスキップ. if (connectedMeshlet == i) continue; // 隣接エッジリストに登録されているかチェック. auto edgeItr = std::find( edgeAdjacency.begin() + offsetEdgeAdjacency, edgeAdjacency.end(), connectedMeshlet); // 未登録. if (edgeItr == edgeAdjacency.end()) { edgeAdjacency.emplace_back(idx_t(connectedMeshlet)); edgeWeights .emplace_back(1); } // 登録済み. else { auto d = std::distance(edgeAdjacency.begin(), edgeItr); assert(d >= 0); edgeWeights[d]++; } } } // メッシュレット開始番号を登録. xAdjacency.push_back(offsetEdgeAdjacency); } xAdjacency.push_back(idx_t(edgeAdjacency.size()));

Slide 88

Slide 88 text

91 void BuildMeshletConectivity ( const std::vector& meshlets, EdgeToMeshletMap& edge2Meshlet, MeshletToEdgeMap& meshlet2Edge ) { // エッジリストを構築. for(size_t mId = 0; mId < meshlets.size(); ++mId) { const auto& meshlet = meshlets[mId]; for(size_t pId = 0; pId < meshlet.Primitives.size(); ++pId) { const auto& prim = meshlet.Primitives[pId]; for(size_t j=0; j<3; ++j) { const auto& e0 = prim.v[j]; const auto& e1 = prim.v[(j + 1) % 3]; if (e0 == e1) continue; Edge edge = { std::min(e0, e1), std::max(e0, e1) }; edge2Meshlet[edge].push_back(mId); meshlet2Edge[mId].emplace_back(edge); } } } // メッシュレットが1つでないものは複数接続されていてボーダーではないので,削除する. { remove_if(edge2Meshlet, [&](const auto& pair) { return pair.second.size() != 1; }); } } using Edge = std::pair; struct EdgeHash { size_t operator() (const Edge& value) const { const auto hasher = std::hash{}; return hasher(value.first) ^ hasher(value.second); } }; using EdgeToMeshletMap = std::unordered_map, EdgeHash>; using MeshletToEdgeMap = std::unordered_map>;

Slide 89

Slide 89 text

• グループ化された各メッシュレットのインデックスバッファを1つにマージ • マージされたメッシュに対して meshoptimizer で簡略化(減ポリ) • この際に,境界エッジを固定化するオプション(meshopt_SimplifyLockBorder)を有効化 • 実装する際の注意点として,meshopt_simplifyで渡す位置座標は全て巡回が実行 そのため,巡回数が少なくなるようにマージ時に数を減らした位置座標を作っておくと良い • 後述するLOD判定のために,meshopt_SimplifyErrorAbsolute を指定して,グループを簡略化する際の 元形状との絶対誤差を取得しておく 92

Slide 90

Slide 90 text

93 uint32_t options = meshopt_SimplifyLockBorder | meshopt_SimplifyErrorAbsolute; float groupError = 0.0f; // Quadratic Error Metrics. float permissiveError = 0.03f; // 許容誤差 (3%未満とする). assert(targetIndexCount > 0); std::vector indices(mergedIdx.size()); auto indexCount = meshopt_simplify( indices.data(), mergedIdx.data(), mergedIdx.size(), &mergedPos[0].x, mergedPos.size(), sizeof(asdx::Vector3), targetIndexCount, permissiveError, options, &groupError); indices.resize(indexCount); mergedIdx.clear(); mergedIdx.shrink_to_fit(); mergedPos.clear(); mergedPos.shrink_to_fit(); // エラー値を設定. result.Error = groupError; // 元の頂点インデックス番号を復元する. result.Indices.reserve(indices.size()); for(const auto& index : indices) { auto vertId = dict[index]; result.Indices.emplace_back(vertId); } // エラーが 0 なければ,マージされて変形した. result.IsMerged = (clusterError > 0.0f); std::unordered_map dict; // mergeIndex <---> verteIndex の辞書. // グループごとにマージする. std::vector mergedIdx; std::vector mergedPos; // 最大数でメモリ確保. mergedIdx.reserve(group.MeshletIds.size() * 256 * 3); mergedPos.reserve(group.MeshletIds.size() * 256); for(size_t i=0; i remap(mergedIdx.size()); auto vertexCount = meshopt_generateVertexRemap( remap.data(), mergedIdx.data(), mergedIdx.size(), mergedPos.data(), mergedPos.size(), sizeof(asdx::Vector3)); // 位置座標をリマップ. { std::vector pos(vertexCount); meshopt_remapVertexBuffer(pos.data(), &mergedPos[0].x, mergedPos.size(), sizeof(asdx::Vector3), remap.data()); mergedPos = std::move(pos); } // 辞書をリマップ. { std::unordered_map remapDict; for(const auto& pair : dict) { auto newIdx = pair.first; auto vertId = pair.second; newIdx = remap[newIdx]; remapDict.try_emplace(newIdx, vertId); } dict = std::move(remapDict); } // 頂点インデックスをリマップ. mergedIdx = std::move(remap); }

Slide 91

Slide 91 text

• ランタイム上で描画するLODを決定したい。 • いくつかの方式が考えられる。 • 従来通りにメッシュ単位でLODを決定する方法(距離ベースなど) • メッシュレット単位でLODを決定する方法 94

Slide 92

Slide 92 text

• 2次誤差尺度(QEM: Quadric Error Metrics)は 頂点ー平面 の2乗距離の総和を表す。 95 𝐸𝑣 = ෍ 𝑝∈𝑝𝑙𝑎𝑛𝑒𝑠(𝑣) 𝑝 ∙ 𝑣 2 = ෍ 𝑝 𝑣⊺𝑝 𝑝⊺𝑣 = 𝑣⊺ ෍ 𝑝 𝑝𝑝⊺ 𝑣 = 𝑣⊺ ෍ 𝑝 𝑄𝑝 𝑣 = 𝑣⊺𝑄𝑣 𝑣 頂点を𝑣 = 𝑥, 𝑦, 𝑧, 1 とし, 𝑄𝑣 = 𝑎2 𝑎𝑏 𝑎𝑐 𝑎𝑑 𝑎𝑏 𝑏2 𝑏𝑐 𝑏𝑑 𝑎𝑐 𝑏𝑐 𝑐2 𝑐𝑑 𝑎𝑑 𝑏𝑑 𝑐𝑑 𝑑2 となる。 平面方程式 𝑝 = 𝑎𝑥 + 𝑏𝑦 + 𝑐𝑧 + 𝑑 を 𝑝 = (𝑎, 𝑏, 𝑐, 𝑑) と表すと,

Slide 93

Slide 93 text

• 親と、所属グループを比べて誤差が指定値が収まっていれば描画 • カメラでズームした際などにも変化するようにするため,射影した画面上での誤差を比較 • これを実現するためにグループ化する際にバウンディング情報を計算して保持しておき, グループのバウンディングと親のバウンディングを求める 96

Slide 94

Slide 94 text

97 float ProjectError(float3 center, float radius, float screenScaleY) { if (isinf(radius)) return radius; // 未初期化データ(= LOD0 の親判定用). // screenScaleY = renderTargetHeight * 0.5f * (1.0f / tan(fov * 0.5f)) とします. float d2 = dot(center, center); return screenScaleY * radius / sqrt(d2 - radius * radius); } bool IsVisibleLod(MeshletInfo meshlet, float pixelErrorThreshold, float4x4 localToView, float screenScaleY) { float4 projGroupSphere = mul(localToView, float4(meshlet.GroupBoundingSphere.xyz, 1.0f)); float projGroupError = ProjectError(projGroupSphere.xyz, max(meshlet.GroupError, 1e-9f), screenScaleY); float4 projParentSphere = mul(localToView, float4(meshlet.ParentBoundingSphere.xyz, 1.0f)); float projParentError = ProjectError(projParentSphere.xyz, max(meshlet.ParentError, 1e-9f), screenScaleY); return (projGroupError <= pixelErrorThreshold && pixelErrorThreshold < projParentError); } 𝑑 𝑙 𝑟 ℎ

Slide 95

Slide 95 text

98

Slide 96

Slide 96 text

• DAG(有向非巡回グラフ)をGPU上で実装するには複雑すぎる • 判定には前述したようにバウンディング情報さえあればいい • 任意の高速化機構が利用できるのでは?(例えばBVHなど) • しかし、ナイーブな実装だと空き状態が出来てしまい,GPUを活用しきれない問題がある [Karis 2021;Takeshige 2018] 99

Slide 97

Slide 97 text

• BVHを例とすると… 理想的には親の処理が終わったら、すぐに子の処理を開始して処理空きの状態を無くしたい • コンピュートから直接,子のスレッドを生成できると良いです。しかし、GPUでは実現するのが難しい • そこで[Karis 2021]では,新しいスレッドを生成させずに, 代わりにスレッドを再利用するPersistent threadsモデルを利用 • GPUのワーカースレッドを生成して,ジョブキューを利用して管理し,キューが空になるまで処理を実行 • ナイーブな実装に比べると25%程度高速化との報告がある[Karis 2021] 100

Slide 98

Slide 98 text

• ジョブキューを実装する方法の一つして,リングバッファを用いた実装が考えられる • 幸いなことに[Mishima 2025]でGPU上での実装が紹介されている 101

Slide 99

Slide 99 text

• 現状はまだ十分な検証が出来ておらず,TODO状態になっています。 • 今後のタイトル開発等で,知見と経験が得られた際に,また皆様の前で報告できればと思います。 102 Image credits: Sue Seecof, https://www.flickr.com/photos/126344637@N05/26013859361, licensed under https://creativecommons.org/licenses/by/2.0/

Slide 100

Slide 100 text

• • • • • • • 103

Slide 101

Slide 101 text

• • • • • • • 104

Slide 102

Slide 102 text

メッシュレットカリング手法などについて 105

Slide 103

Slide 103 text

• 仮想化ジオメトリシステムを用いてマイクロポリゴンを大量に描画したい • 従来よりもポリゴン数などは非常に多くなり,効率的に描画する手法が必要 • 効率的に描画するにはどうすればいいでしょうか? • メッシュストリーミングを利用すると毎フレームデータを読み込む可能性がある • データが大きいと読み込む時間は長くなるため、できるだけコンパクトにしたい • 描画されないポリゴンデータは無駄な処理負荷になるので当然排除したい 106

Slide 104

Slide 104 text

(1) 描画処理時間を短くする (2) 読込速度や転送速度を速くする 107

Slide 105

Slide 105 text

108

Slide 106

Slide 106 text

• 視錐台カリング • 寄与カリング • 法錐カリング • 背面カリング • 微小プリミティブカリング • オクルージョンカリング …などなど。 • 最近ではNeural Networkを用いてカリングをする試みもある [Yu 2024; Yu 2025] 109

Slide 107

Slide 107 text

増幅シェーダを用いたカリング手法 110

Slide 108

Slide 108 text

• ビューボリュームの範囲外のオブジェクトを描画対象から外す方法[Honda 2019] • 良くある事例のひとつとして,背景オブジェクトが大きすぎて,うまく視錐台カリング出来ない 問題がある[Honda 2019; Mishima 2018] 111

Slide 109

Slide 109 text

• うまくカリングするために従来はメッシュを分割する必要があった • これをメッシュレットで代用すればよいのでは? • メッシュレットにバウンディング情報を持たせてカリング • 実装方法がいくつ考えられる • • • • 112

Slide 110

Slide 110 text

• メッシュシェーダ実行直前にカリングを実行 • カリング結果に応じて,メッシュシェーダを起動するかどうかを 制御できるため,結果を保存しなくても良いので省メモリ化 • 描画するメッシュレット数をカウントして,ペイロードとしてメッシュシェーダに渡す 114 struct Payload; groupshread Payload g_Payload; [numthreads(32, 1, 1)] void main(uint dispatchId : SV_DispatchThreadID) { … DispatchMesh(visibleCount, 1, 1, g_Payload); } [outputtopology("triangle")] [numthreads(128, 1, 1)] void main( uint threadId : SV_GroupThreadID, uint groupId : SV_GroupID, in payload Payload payload, out vertices MSOutput vertices[256], out indices uint3 indices [256]) { … }

Slide 111

Slide 111 text

115 bool Contains(float4 planes[6], float4 sphere) { // sphereは事前に位置座標がワールド変換済み,半径もスケール適用済みとします. float4 center = float4(sphere.xyz, 1.0f); for(int i=0; i<6; ++i) { if (dot(center, planes[i]) < -sphere.w) return false; // カリングする. } // カリングしない. return true; }

Slide 112

Slide 112 text

116 [numthreads(32, 1, 1)] void main(uint dispatchId : SV_DispatchThreadID) { bool visible = false; if (dispatchId < g_Constants.MeshletCount) { MeshletInfo info = g_Meshlets[dispatchId]; MeshInstanceParam instance = g_MeshInstances[g_Constants.InstanceId]; visible = IsVisible( info, g_TransParam.CameraPos, g_TransParam.Planes, instance.CurrWorld, g_TransParam.ViewProj); } if (visible) { uint index = WavePrefixCountBits(visible); g_Payload.MeshletIndices[index] = dispatchId; } uint visibleCount = WaveActiveCountBits(visible); DispatchMesh(visibleCount, 1, 1, g_Payload); } Wave Intrinsics

Slide 113

Slide 113 text

• 自身のLane Index未満のActive Laneで引数にtrueを指定した個数を返却 • uint WavePrefixCountBits(bool bBit); 117 Wave bool visible = true; WavePrefixCountBits(visible) return 0; bool visible = false; WavePrefixCountBits(visible) return 1; bool visible = true; WavePrefixCountBits(visible) return 1; bool visible = true; WavePrefixCountBits(visible) return 2; bool visible = false; WavePrefixCountBits(visible) return 2; bool visible = true; WavePrefixCountBits(visible) return 3; Inactive Lane Active Lane

Slide 114

Slide 114 text

• 引数にtrueを指定したLaneの数を,すべてのActive Laneに返却 • uint WaveActiveCountsBit(bool bBit); 118 Wave WaveActiveCountBits(true); WaveActiveCountBits(false); WaveActiveCountBits(true); WaveActiveCountBits(true); WaveActiveCountBits(true); WaveActiveCountBits(false); true WaveActiveCountBits()

Slide 115

Slide 115 text

119 [numthreads(32, 1, 1)] void main(uint dispatchId : SV_DispatchThreadID) { bool visible = false; if (dispatchId < g_Constants.MeshletCount) { MeshletInfo info = g_Meshlets[dispatchId]; MeshInstanceParam instance = g_MeshInstances[g_Constants.InstanceId]; visible = IsVisible( info, g_TransParam.CameraPos, g_TransParam.Planes, instance.CurrWorld, g_TransParam.ViewProj); } if (visible) { uint index = WavePrefixCountBits(visible); g_Payload.MeshletIndices[index] = dispatchId; } uint visibleCount = WaveActiveCountBits(visible); DispatchMesh(visibleCount, 1, 1, g_Payload); }

Slide 116

Slide 116 text

• メッシュレットが小さすぎて,ピクセルに寄与しない場合に,カリングする[Jansson 2024] • シャドウマップにメッシュレットを描画する際に使用すると良い • 具体的にはスクリーンに投影したバウンディング領域のx, yの長さを調べて, 指定閾値未満ならカリングを実行 120 float4 LBRT = SphereScreenExtents(boundingSphere, WorldToClip); float w = abs(LBRT.z – LBRT.x); // (left – right) float h = abs(LBRT.w – LBRT.y); // (top – bottom) culled |= max(w, h) < minContribution;

Slide 117

Slide 117 text

• メッシュレット単位で,背面カリングを行う手法 • 各面ごとの法線ベクトルを束ねて,すべての法線を内包する円錐をつくる。 この円錐を使って背面カリングを行う 121

Slide 118

Slide 118 text

<もっとも簡単な作り方> • 各ポリゴン法線から平均法線を求め,中心軸とする • 中心軸と各ポリゴン法線との内積を求めて,最大となるものを角度とする <厳密な最小法錐の作り方の例> • 各法線ベクトルを3次元空間上の点としてみなす • 各点からすべてを内包する最小の球冠(Spherical Cap)を求める • 求めた球冠の中心を法錐の中心軸,開口角を法錐の角度とする <meshOptimizerを使った作り方> • meshopt_computeMeshletBounds()関数を利用する 122

Slide 119

Slide 119 text

123 𝛼

Slide 120

Slide 120 text

124 𝛼

Slide 121

Slide 121 text

125 𝛼 cos 𝜋 2 + 𝛼 = − sin 𝛼 dot(-viewDir, coneAxis) > -sin(coneAngle)) cos 𝜃 > cos 𝜋 2 + 𝛼 𝜃 coneAngle

Slide 122

Slide 122 text

126 bool IsVisible(MeshletInfo info, float3 cameraPos, float4 planes[6], float4x4 world, float4x4 viewProj) { // [-1, 1] に展開. float4 normalCone = UnpackUnorm(info.NormalCone) * 2.0f - 1.0f; // 角度がゼロかどうか判定. if (normalCone.w <= 1e-6f) return false; // ワールド空間に変換. float4 sphere = TransformSphere(info.BoundingSphere, world); float3 axis = normalize(mul((float3x3)world, normalCone.xyz)); // 視錐台カリング. if (!Contains(planes, sphere)) return false; // 寄与カリング. if (ContributionCulling(sphere, viewProj)) return false; // 視線ベクトルを求める. float3 viewDir = normalize(sphere.xyz - cameraPos); // 法錐カリング. if (NormalConeCulling(float4(axis, normalCone.w), viewDir)) return false; // カリングしない. return true; } float4 TransformSphere(float4 sphere, float4x4 world) { // 中心をワールド変換. float3 center = mul((float3x3)world, sphere.xyz); // 各軸の長さの2乗値を求める. float sx = dot(world._11_12_13, world._11_12_13); float sy = dot(world._21_22_23, world._21_22_23); float sz = dot(world._31_32_33, world._31_32_33); // 最も大きいものをスケールとして採用. float scale = sqrt(max(sx, max(sy, sz))); return float4(center, sphere.w * scale); }

Slide 123

Slide 123 text

127

Slide 124

Slide 124 text

メッシュシェーダを用いたカリング手法 128

Slide 125

Slide 125 text

• 計算量とのトレードオフになるので,処理計測して効果を確認する • 場合によっては計算量が多くなり、重くなる可能性もある • 以下の手法を紹介します • • • • 129

Slide 126

Slide 126 text

• 行列式(det)は幾何学的に「体積」や「面積」を表す量 • 符号付き面積・あるいは体積と解釈できるので表面か背面かを判定 • 3x3の行列式の値はスカラー三重積と等しくなるので, 外積と内積を使って簡単に計算可能 • 0との比較により,ゼロ面積を判定 130 bool IsBackFaceOrZeroArea(float3 v0, float3 v1, float3 v2, float3 viewDir) { float3 a = v1 – v0; float3 b = v2 – v0; float3 n = cross(a, b); return dot(n, viewDir) >= 0.0f; // true ならカリングする. }

Slide 127

Slide 127 text

• スクリーン空間に変換し,[0, 1]の範囲内に収まっているかどうかチェックし, 収まっていなかったらカリングする • スクリーン空間でのAABBを求めて,チェック 131 float2 minAABB = 1.0f.xx; float2 maxAABB = 0.0f.xx; for(uint i=0; i<3; ++i) { float2 posSS = (projPos[i].xy / projPos[i].w); posSS = posSS * 0.5f + 0.5f; // [0, 1]に変換. minAABB = min(minAABB, posSS); maxAABB = max(maxAABB, posSS); } culled = any(minAABB > 1.0) || any(maxAABB < 0.0); 0.0 1.0 1.0 maxAABB.x < 0.0 minAABB.x > 1.0 minAABB.y > 1.0 maxAABB.y < 0.0

Slide 128

Slide 128 text

• ピクセル化されないポリゴンをラスタライズ前にカリング • 事前にカリングすることでラスタライズ効率化 [Wihlidal 2017] 132 minAABB *= GetRenderTargetSize(); maxAABB *= GetRenderTargetSize(); culled = any(round(minAABB) == round(maxAABB));

Slide 129

Slide 129 text

• 前述したように,SetMeshOutputCounts()の数は動的に変更できない (仕様に沿っていないため,コンパイルエラーなどになる) • そのため,SV_CullPrimitive セマンティクスを利用してカリング • Primitives属性として,プリミティブ単位で出力をすればいい 133 struct PrimOutput { float4 Color : COLOR0; bool Culling : SV_CullPrimitive; }; [outputtopology("triangle")] [numthreads(128, 1, 1)] void main ( uint threadId : SV_GroupThreadID, uint groupId : SV_GroupID, in payload Payload payload, out vertices MSOutput vertices[256], out indices uint3 indices [256], out primitives PrimOutput prims [256] )

Slide 130

Slide 130 text

134 [outputtopology("triangle")] [numthreads(128, 1, 1)] void main ( uint threadId : SV_GroupThreadID, uint groupId : SV_GroupID, in payload Payload payload, out vertices MSOutput vertices[256], out indices uint3 indices [256], out primitives PrimOutput prims [256] ) { uint meshletIndex = payload.MeshletIndices[groupId]; MeshletInfo info = g_Meshlets[meshletIndex]; SetMeshOutputCounts(info.VertexCount, info.PrimitiveCount); MeshInstanceParam instanceParam = g_MeshInstances[g_Constants.InstanceId]; for(uint i=0; i<2; ++i) { uint id = threadId + i * 128; if (id < info.PrimitiveCount) { // プリミティブインデックスを設定. uint3 tris = GetPrimitiveIndex(id + info.PrimitiveOffset); float3 posW [3]; // カリング用ワールド位置座標. float2 posSS[3]; // カリング用スクリーン空間座標. for (uint j = 0; j < 3; ++j) { /* 頂点データ処理の実装 */ } // カリング処理. bool culled = false; culled |= IsBackFaceOrZeroArea(posW, g_TransParam.CameraPos); culled |= PrimitiveCulling(posSS); // プリミティブアトリビュートを出力. PrimOutput output = (PrimOutput) 0; output.Color.rgb = ToSRGB(HueToRGB(groupId * 1.71f)); output.Color.a = 1.0f; output.Culling = culled; prims[id] = output; } } } uint idx = tris[j]; // 頂点数を超える場合は処理しない. if (idx >= info.VertexCount) continue; float4 localPos = float4(g_Positions[idx + info.VertexOffset], 1.0f); float4 worldPos = mul(instanceParam.CurrWorld, localPos); float4 viewPos = mul(view, worldPos); float4 projPos = mul(proj, viewPos); float3 localNormal = g_Normals[idx + info.VertexOffset]; float3 worldNormal = normalize(mul((float3x3) instanceParam.CurrWorld, localNormal)); MSOutput output; output.Position = projPos; output.Normal = worldNormal; output.TexCoord = g_TexCoords[idx]; vertices[idx] = output; posW [j] = worldPos.xyz; posSS[j] = (projPos.xy / projPos.w) * 0.5f + 0.5f; bool PrimitiveCulling(float2 posSS[3]) { bool culled = false; float2 mini = 1.0f.xx; float2 maxi = 0.0f.xx; // 視錐台カリング. for (uint i = 0; i < 3; ++i) { mini = min(mini, posSS[i]); maxi = max(maxi, posSS[i]); } culled |= (any(mini > 1.0f) || any(maxi < 0.0f)); // カリングする. // 微小プリミティブカリング. maxi *= g_TransParam.RenderTargetSize.xy; mini *= g_TransParam.RenderTargetSize.xy; culled |= any(round(mini) == round(maxi)); // カリングする. return culled; }

Slide 131

Slide 131 text

(1) 描画処理時間を短くする (2) 読込速度や転送速度を速くする 135

Slide 132

Slide 132 text

(1) 描画処理時間を短くする (2) 読込速度や転送速度を速くする 136

Slide 133

Slide 133 text

137

Slide 134

Slide 134 text

• Octahedron Encoding [Cigolle 2014] • 球面上の法線を八面体にマッピング 138 float2 OctWrap(float2 v) { return (1.0f – abs(v.yx)) * select(v.xy >= 0.0f, 1.0f, -1.0f); } float2 PackNormal(float3 v) { float3 n = normal / (abs(v.x) + abs(v.y) + abs(v.z)); n.xy = select(n.z >= 0.0f, n.xy, OctWrap(n.xy)); return n.xy * 0.5f + 0.5f; } float3 UnpackNormal(float2 v) { float2 enc = v * 2.0f – 1.0f; float3 n = float3(enc, 1.0f – abs(enc.x) – abs(enc.y)); float t = saturate(-n.z); n.xy += select(n.xy >= 0.0f, -t, t); return normalize(n); }

Slide 135

Slide 135 text

• 上下対称なので,符号ビットを持てば,精度を2倍に出来る[John White 3D 2017] 139

Slide 136

Slide 136 text

140 struct PackedNormal { float2 normal; // 圧縮済み法線ベクトル. uint sign; // 符号ビット. }; PackedNormal SignedOctEncode(float3 n) { PackedNormal result; n /= (abs(n.x) + abs(n.y) + abs(n.z)); result.normal.y = n.y * 0.5f + 0.5f; result.normal.x = n.x * 0.5f + result.normal.y; result.normal.y = n.x * -0.5f + result.normal.y; result.sign = (uint)saturate(n.z * FLT_MAX); return result; } float3 SignedOctDecode(PackedNormal v) { float3 result; result.x = (v.normal.x – v.normal.y); result.y = (v.normal.x + v.normal.y) – 1.0f; result.z = (float)v.sign * 2.0f – 1.0f; result.z = result.z * (1.0f – abs(result.x) – abs(result.y)); return normalize(result); }

Slide 137

Slide 137 text

• 接線ベクトルを角度𝛼として表し、次元数を減らす [Geffroy 2020] 141

Slide 138

Slide 138 text

• 八面体マッピングと同じアイデアで, 角度をダイアモンドエンコーディング[Ong 2023]で表す 142 0.0 1.0

Slide 139

Slide 139 text

143 float EncodeDiamond(float2 v) { float m = abs(v.x) + abs(v.y); if (m == 0.0f) return 0.0f; float x = v.x / m; float s = sign(v.x); return –s * 0.25 * x + 0.5f + s * 0.25f; } float EncodeTangent(float3 normal, float3 tangent) { float3 t1; if (abs(normal.y) > abs(normal.z)) t1 = float3(normal.y, -normal.x, 0.0f); else t1 = float3(normal.z, 0.0f, -normal.x); t1 = normalize(t1); float3 t2 = cross(t1, normal); float2 packedTangent = float2( dot(tangent, t1), dot(tangent, t2)); return EncodeDiamond(packedTangent); } float2 DecodeDiamond(float v) { if (v == 0.0f) return float2(0.0f, 0.0f); float2 result; float s = sign(v – 0.5f); result.x = -s * 4.0f * v + 1.0f + s * 2.0f; result.y = s * (1.0f – abs(result.x)); return normalize(result); } float3 DecodeTangent(float3 normal, float diamondTangent) { float3 t1; if (abs(normal.y) > abs(normal.z)) t1 = float3(normal.y, -normal.x, 0.0f); else t1 = float3(normal.z, 0.0f, -normal.x); t1 = normalize(t1); float3 t2 = cross(t1, normal); float2 packedTangent = DecodeDiamond(diamondTangent); return packedTangent.x * t1 + packedTangent.y * t2; }

Slide 140

Slide 140 text

144 float EncodeDiamond(float2 v) { float m = abs(v.x) + abs(v.y); float x = v.x / m; float s = sign(v.x); return -s * 0.25f * x + 0.5f + s * 0.25f; } float EncodeTangent(float3 normal, float3 tangent) { float3 t1; if (abs(normal.y) > abs(normal.z)) t1 = float3(normal.y, -normal.x, 0.0f); else t1 = float3(normal.z, 0.0f, -normal.x); t1 = normalize(t1); float3 t2 = cross(t1, normal); float2 packedTangent = float2( dot(tangent, t1), dot(tangent, t2)); return EncodeDiamond(packedTangent); } float2 DecodeDiamond(float v) { float2 result; float s = sign(v – 0.5f); result.x = -s * 4.0f * v + 1.0f + s * 2.0f; result.y = s * (1.0f – abs(v.x)); return normalize(v); } float3 DecodeTangent(float3 normal, float diamondTangent) { float3 t1; if (abs(normal.y) > abs(normal.z)) t1 = float3(normal.y, -normal.x, 0.0f); else t1 = float3(normal.z, 0.0f, -normal.x); t1 = normalize(t1); float3 t2 = cross(t1, normal); float2 packedTangent = DecodeDiamond(diamondTangent); return packedTangent.x * t1 + packedTangent.y * t2; }

Slide 141

Slide 141 text

• Signed Octahedron Encodingと同じで精度改善可能 • 符号ビットを持たせることで,精度を倍に 145 SignedDiamond EncodeDiamond(float2 p) { SignedDiamond result; float m = abs(p.x) + abs(p.y); if (m == 0.0f) { result.sign = false; result.angle = 0.0f; return result; } float x = p.x / m; result.sign = (p.y >= 0.0f) ? true : false; result.angle = x * 0.5f + 0.5f; return result; } struct SignedDiamond { float angle; // エンコード済み角度. bool sign; // true = 1.0f, false = -1.0f. }; float2 DecodeDiamond(SignedDiamond p) { if(p.angle == 0.0f && !p.sign) return float2(0.0f, 0.0f); float2 result; float s = p.sign ? 1.0f : -1.0f; result.x = 2.0f * p.angle – 1.0f; result.y = s * (1.0f – abs(result.x)); return normalize(result); }

Slide 142

Slide 142 text

• Signed Octahedron Encodingと同じで精度改善可能 • 符号ビットを持たせることで,精度を倍に 146 SignedDiamond EncodeDiamond(float2 p) { SignedDiamond result; float m = abs(p.x) + abs(p.y); float x = p.x / m; result.sign = (p.y >= 0.0f) ? true : false; result.angle = x * 0.5f + 0.5f; return result; } struct SignedDiamond { float angle; // エンコード済み角度. bool sign; // true = 1.0f, false = -1.0f. }; float2 DecodeDiamond(SignedDiamond p) { float2 result; float s = p.sign ? 1.0f : -1.0f; result.x = 2.0f * p.angle – 1.0f; result.y = s * (1.0f – abs(result.x)); return normalize(result); }

Slide 143

Slide 143 text

• ShaderX5に計算により復元する方法が紹介されている[Schüler2007] • 微分値を用いてピクセルシェーダにて接線・従接線データを求める • 改良方法が[Schüler2013]で紹介されている 147 float3x3 tangent_frame(float3 N, float3 p, float2 uv) { float3 dp1 = ddx(p); float3 dp2 = ddy(p); float2 duv1 = ddx(uv); float2 duv2 = ddy(uv); float2x3 M = float2x3(dp1, dp2); float3 T = mul(float2(duv1.x, duv2.x), M); float3 B = mul(float2(duv1.y, duv2.y), M); return float3x3(normalize(T), normalize(B), N); } float3x3 cotangent_frame(float3 N, float3 p, float2 uv) { float3 dp1 = ddx(p); float3 dp2 = ddy(p); float2 duv1 = ddx(uv); float2 duv2 = ddy(uv); float3 dp2perp = cross(dp2, N); float3 dp1perp = cross(N, dp1); float3 T = dp2perp * duv1.x + dp1perp * duv2.x; float3 B = dp2perp * duv1.y + dp1perp * duv2.y; float invmax = rsqrt(max(dot(T, T), dot(B, B)); return float3x3(T * invmax, B * invmax, N); }

Slide 144

Slide 144 text

• 位置座標などは前述したような八面体エンコードなどが使えない • そこで,量子化によりデータサイズの削減を行う • 量子化とは,デジタル信号の精度を高精度の形式から低精度の形式に下げるプロセス。 精度を落すことによりメモリ使用量を削減 148 (1.34, 2.10) (1.34, 1.20) (2.57, 2.10) (2.57, 1.20) 0.9 1.23 (0, 0) (0, 1) (1, 1) (1, 1) float2 uint8_t2 (1.34, 1.20)

Slide 145

Slide 145 text

• 量子化することで,データ量を減らす[Kuth2024; AMD 2024] • 隣接メッシュレットとエッジが一致するように気を付けないとクラック(穴あき)が発生 • 尺度を統一させてから,各軸ごとに量子化を行うことで,クラック対策を行う 149

Slide 146

Slide 146 text

150 const auto& kMesh = …; const auto kTargetBits = 16; const float3 kGlobalMin = kMesh.GetAABB().min; const float3 kGlobalDelta = kMesh.GetAABB().max – kGlobalMin; float3 largestMeshletDelta = 0.0f.xxx; for(const auto& meshlet : kMesh.GetMeshlets()) { const float3 delta = meshlet.GetAABB().max – mehlet.GetAABB().min; largestMeshletDelta = max(largestMeshletDelta, delta); } const float3 kMeshletQuantizationStep = largestMeshletDelta / ((1 << kTargetBits) – 1); const uint32_t3 kGlobalQuantizationSattes = kGlobalDelta / kMeshletQuantizationStep; const float3 kQuantizationFactor = (kGlobalQunaizationSates – 1) / kGlobalDelta; const float3 kDequantizationFactor = kGlobalDelta / (kGlobalQunatizationStates – 1); for(const auto& mehlet : kMesh.GetMeshlets()) { const uint32_t3 kQuantizationMeshletOffset = uint32_t3(meshlet.GetAABB().min – kGlobalMin) * kQuantizaitonFactor + 0.5f); for(const auto& vertex : meshlet.GetVertices()) { const float3 value = vertex; const uint32_t3 kGlobalQuantizedValue = uint32_t3((value – kGlobalMin) * kQuantizationFactor + 0.5f); const uint32_t3 kLocalQuantizedValue = kGlobalQuantizedValue – kQuantizedMeshletOffset; output.quantizedPosition[i++] = uint16_t3(kLocalQuantizedValue); // ローカル量子化値を保存. } outMeshlet.OffsetPosition = kQuantizationMeshletsOffset; // 各メッシュレットについて軸ごとの量子化メッシュレットオフセットを保存. } // 逆量子化のため,係数とkGlobalMinを保存。 output.Base = kGlobalMin; output.Factor = kDequantizationFactor;

Slide 147

Slide 147 text

• 先ほど計算した, ・kDequantizationFactor (逆量量子化スケール) ・kGlobalMin (全体の基点位置) ・kQuantizedMeshletOffset (メッシュレットの基点) を使用して,localQuantizedValueから次のように逆量子化 151 float3 value = (kQuantizedMeshletOffset + localQuantizedValue) * kDequantizationFactor + kGlobalMin;

Slide 148

Slide 148 text

• 前述したように,メッシュシェーダは最大256頂点までしか出力できない • そのため,頂点インデックスバッファは8-bitあれば十分 • 従来32-bitをインデックスを用いていた場合は1/4程度に削減可能 • 8-bitインデックスを使用する場合は,シェーダ上でのアクセス注意が必要 (ByteAddressBufferは通常4byteアライメント) 152 0 1 2 0 2 3 3 4 5 …

Slide 149

Slide 149 text

• 割と愚直な実装ですが,実装例は下記の通り 153 uint3 GetPrimitiveIndex(uint triangleIndex) { // 3バイト単位で三角形のインデックスが格納されている. uint baseByteOffset = triangleIndex * 3; // 各インデックスへのバイトオフセット. uint3 byteOffset = uint3(baseByteOffset + 0, baseByteOffset + 1, baseByteOffset + 2); // 各インデックスが含まれる4バイト境界を計算. uint3 alignedOffset = byteOffset & (~3u).xxx; // バイトをまたぐ場合を考慮して,8バイトロード. uint2 raw = g_Primitives.Load2(alignedOffset.x); // 必要なデータを決定. uint3 data; data.x = raw.x; data.y = (alignedOffset.y != alignedOffset.x) ? raw.y : data.x; data.z = (alignedOffset.z != alignedOffset.y) ? raw.y : data.y; // シフト量 uint3 shift = (byteOffset & (3u).xxx) * 8; // 頂点インデックスを抽出. return (data >> shift) & 0xFF.xxx; }

Slide 150

Slide 150 text

• 前ページの実装は動作はしますが,SRVバッファへのデータアクセスが無駄に発生する • こうした無駄も排除したいのであれば,一度データアクセスしてgroupsharedに格納 • SRVバッファへのデータアクセス回数を減らし, groupsharedに格納されたものに対して, ビット演算等を使って処理することで,処理改善が見込める 154 groupshared X X X X X X X X groupshared

Slide 151

Slide 151 text

• 三角形ストリップ化し,L/Rフラグとインクリメントのフラグを持ち表現 • シェーダは1bitデータで持てないため,メッシュレット単位でまとめてデータを持つこと により,シェーダでアクセスできる形で保持する 155

Slide 152

Slide 152 text

(1) 描画処理時間を短くする (2) 読込速度や転送速度を速くする 156

Slide 153

Slide 153 text

(1) 描画処理時間を短くする (2) 読込速度や転送速度を速くする 157

Slide 154

Slide 154 text

• • • • • • • 158

Slide 155

Slide 155 text

• • • • • • • 159

Slide 156

Slide 156 text

Deferred Materialの導入 160

Slide 157

Slide 157 text

• Deferred RenderingはG-Bufferの登場と共にともに以下の理由で主流に • 複数の動的ライトを効率よく利用可能 • ライティングの独立性(ジオメトリ・マテリアルとは独立して処理可能) • シャドウやスクリーンスペースエフェクト(SSAOやSSR)などと統合しやすい • 一方で、流行して使われるようになるとともに、問題点も明らかに 161

Slide 158

Slide 158 text

• 問題点1:G-Bufferの帯域/容量の肥大化 • 法線・アルベド・メタルネス・ラフネス・エミッシブなどを保持するために複数のレンダーターゲットが必要 • 4Kといった高解像度な環境では,帯域がボトルネックになりやすい(GPUメモリ転送・キャッシュ負荷) • 問題点2:マテリアル多様性の制約 • Deferred Renderingでは全マテリアルが共通のG-Buffer構造を強制 • 多彩なマテリアルを扱いづらい・異なるBRDFを同時に扱いづらい(clear coat, anisotropyなど) 162

Slide 159

Slide 159 text

Deferred Renderingとは別の話になりますが… 「頂点シェーダ × ピクセルシェーダ」 の組み合わせ数によりシェーダバリエーション数が増大するという問題 • シェーダコンパイル時間が長くなるのは開発効率を落とす原因の1つ • 最適化のためにゲーム開始時にシェーダをコンパイルするようなPCゲーム等の場合は, 「中々ゲームが起動しない」「やりたくてもゲームが開始出来ない」といった ユーザーにストレスを与える原因に… • 多彩なマテリアルを表現するためにはシェーダを増やす必要もある 163

Slide 160

Slide 160 text

• 帯域を減らすために、出来る限りG-Bufferは小さくしたい • ジオメトリとマテリアルを完全分離できればシェーダコンパイル問題が解決できそう • 多彩なマテリアルを表現するために,G-Bufferで共通化しないといけない制限を止めたい • マテリアルごとに必要な情報にだけアクセスできるようにしたい 164

Slide 161

Slide 161 text

• 「必要になるまでマテリアル情報をフェッチしない/評価しない」という考え方 • Deferred Material/Deferred Textureを実現する手法として, Visibility Buffer(V-Buffer)を採用する事例が登場 [Karis 2021; Mclaren 2022; Mishima 2025] 165 ©Sony Interactive Entertainment Inc.

Slide 162

Slide 162 text

G-Bufferの肥大化は避けたい。でも、マテリアルはいっぱい使いたい! • G-Bufferを三角形番号とインスタンスIDを格納するだけのVisibility Buffer(V-Buffer)に 置き換えることでデータサイズを小さくすることにより問題に対処 • V-Bufferを使用することによりG-Bufferに比べて帯域幅とストレージが削減可能 • G-Bufferを生成するよりもコストは低く,生成時にテクスチャの読み込みやマテリアル固有の 計算などは基本的には必要としない 166 R 1-bit Alpha-Mask 8-bit DrawID 23-bit TriangleID / PrimitiveID V-Buffer

Slide 163

Slide 163 text

167 Depth Prepass G-Buffer Pass Lighting Pass

Slide 164

Slide 164 text

168 Depth Prepass G-Buffer Pass Lighting Pass

Slide 165

Slide 165 text

169 Depth Prepass G-Buffer Pass Lighting Pass V-Buffer Pass Classify Pass Lighting Pass Lighting Pass Lighting Pass Lighting Pass

Slide 166

Slide 166 text

170 Depth Prepass G-Buffer Pass Lighting Pass V-Buffer Pass Classify Pass Lighting Pass Lighting Pass Lighting Pass Lighting Pass

Slide 167

Slide 167 text

171

Slide 168

Slide 168 text

• 今回の実装では,Visibility Bufferの描画にメッシュシェーダを使用 • その他の処理については、すべてコンピュートシェーダで実装 • 今回コンピュートシェーダを実装方式に選んだ理由としては, 非同期コンピュートに処理を逃がせるという利点があるため • 他の実装法としては,[Karis 2021]や[Mishima 2025],そして[Doghramachi2017]のように 深度バッファをハックして,タイル描画を行い必要なシェーダを起動させるという方法がある 172 V-Buffer Pass Classify Pass Lighting Pass Lighting Pass Lighting Pass Lighting Pass

Slide 169

Slide 169 text

173 WorkList Buffer

Slide 170

Slide 170 text

174

Slide 171

Slide 171 text

175

Slide 172

Slide 172 text

• メッシュシェーダで描画するために必要なインスタンスIDや三角形IDなどは揃っているので, これをそのままピクセルシェーダに渡し,V-Bufferに書き込む • 今回の実装では,R32G32_UINTフォーマットとして,IDを書き出す 176 struct MSOutput { float4 Position : SV_Position; uint InstanceId : INSTANCE_ID; uint MeshletId : MESHLET_ID; uint PrimitiveId : PRIMITIVE_ID; }; struct PSOutput { uint2 VisBuffer : SV_Target0; }; PSOutput main(const MSOutput input) { PSOutput output = (PSOutput)0; output.VisBuffer.x = ((input.InstanceId & 0xffffff) << 8) | (input.PrimitiveId & 0xff); output.VisBuffer.y = input.MeshletId; return output; }

Slide 173

Slide 173 text

177

Slide 174

Slide 174 text

178

Slide 175

Slide 175 text

• シェーダごとに必要なピクセルだけを起動させてライティング処理を行いたい • そのために,シェーダ番号とピクセル番号をペアにした処理対象リストを作成 • 処理対象リストからシェーダごとに起動が必要なピクセル数をカウントし,オフセットに • 算出したオフセットを利用して,シェーダごとに固まるようにソート処理を実行 179

Slide 176

Slide 176 text

• シェーダごとに必要なピクセルだけを起動させてライティング処理を行いたい • そのために,シェーダ番号とピクセル番号をペアにした処理対象リストを作成 • 処理対象リストからシェーダごとに起動が必要なピクセル数をカウントし,オフセットに • 算出したオフセットを利用して,シェーダごとに固まるようにソート処理を実行 180 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4

Slide 177

Slide 177 text

• シェーダごとに必要なピクセルだけを起動させてライティング処理を行いたい • そのために,シェーダ番号とピクセル番号をペアにした処理対象リストを作成 • 処理対象リストからシェーダごとに起動が必要なピクセル数をカウントし,オフセットに • 算出したオフセットを利用して,シェーダごとに固まるようにソート処理を実行 181 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 2 3 2 1

Slide 178

Slide 178 text

• シェーダごとに必要なピクセルだけを起動させてライティング処理を行いたい • そのために,シェーダ番号とピクセル番号をペアにした処理対象リストを作成 • 処理対象リストからシェーダごとに起動が必要なピクセル数をカウントし,オフセットに • 算出したオフセットを利用して,シェーダごとに固まるようにソート処理を実行 182 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 2 3 2 1 2 0 5 7 8

Slide 179

Slide 179 text

• シェーダごとに必要なピクセルだけを起動させてライティング処理を行いたい • そのために,シェーダ番号とピクセル番号をペアにした処理対象リストを作成 • 処理対象リストからシェーダごとに起動が必要なピクセル数をカウントし,オフセットに • 算出したオフセットを利用して,シェーダごとに固まるようにソート処理を実行 183 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 2 3 2 1 PixelId : 0 ShaderId : 2 PixelId : 4 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 6 ShaderId : 4 PixelId : 3 ShaderId : 5 2 0 5 7 8

Slide 180

Slide 180 text

184 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 0 0 0 0 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 2 5 7 8 0 6 0

Slide 181

Slide 181 text

185 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 1 0 0 0 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 2 5 7 8 0 6 PixelId : 0 ShaderId : 2

Slide 182

Slide 182 text

186 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 1 0 0 0 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 2 5 7 8 0 6 PixelId : 0 ShaderId : 2 2

Slide 183

Slide 183 text

187 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 1 1 0 0 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 2 5 7 8 0 6 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3

Slide 184

Slide 184 text

188 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 1 1 0 0 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 2 5 7 8 0 6 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 5

Slide 185

Slide 185 text

189 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 1 1 1 0 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 2 5 7 8 0 6 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4

Slide 186

Slide 186 text

190 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 1 1 1 0 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 2 5 7 8 0 6 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 7

Slide 187

Slide 187 text

191 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 1 1 1 1 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 2 5 7 8 0 6 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5

Slide 188

Slide 188 text

192 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 1 1 1 1 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 2 5 7 8 0 6 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 1

Slide 189

Slide 189 text

193 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 2 1 1 1 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 2 5 7 8 0 6 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2

Slide 190

Slide 190 text

194 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 2 1 1 1 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 2 5 7 8 0 6 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 3

Slide 191

Slide 191 text

195 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 2 2 1 1 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 2 5 7 8 0 6 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3

Slide 192

Slide 192 text

196 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 2 2 1 1 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 2 5 7 8 0 6 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 6

Slide 193

Slide 193 text

197 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 2 2 2 1 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 2 5 7 8 0 6 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 6 ShaderId : 4

Slide 194

Slide 194 text

198 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 2 2 2 1 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 2 5 7 8 0 6 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 6 ShaderId : 4 4

Slide 195

Slide 195 text

199 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 2 3 2 1 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 2 5 7 8 0 6 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4

Slide 196

Slide 196 text

200 [numthreads(8, 8, 1)] void main(uint3 dispatchId : SV_DispatchThreadID, uint groupIndex : SV_GroupIndex) { // ピクセル座標. const uint2 pixelId = RemapLane8x8(dispatchId.xy, groupIndex); const uint flatPixelId = (pixelId.x & 0xFFFF) | ((pixelId.y & 0xFFFF) << 16); // レンダーターゲットサイズを超えている場合は処理しない. if (any(pixelId >= g_Constants.RenderTargetSize)) return; // ビジビリティバッファからメッシュレットIDを取得. uint meshletId = g_VisibilityBuffer[pixelId].y; if (meshletId == 0) // 0は無効値(=書き込みされていない扱い). return; meshletId--; // 1始まりなので0始まりに戻す. // メッシュレット情報からシェーダIDを取得. const uint shaderId = (g_Meshlets[meshletId].MaterialId >> 16) & 0xFFFF; // シェーダ数を数える. InterlockedAdd(g_ShaderIdCounter[shaderId], 1); // 書き込み番号を取得. int index = 0; InterlockedAdd(g_WorkListCounter[0], 1, index); WorkItem item; item.ShaderId = shaderId; item.FlatPixelId = flatPixelId; g_WorkList[index] = item; } struct Constants { uint2 RenderTargetSize; uint MaxShaderCount; uint Reserved; }; struct WorkItem { uint ShaderId; uint FlatPixelId; }; struct MeshletInfo { uint VertexOffset; uint VertexCount; uint PrimitiveOffset; uint PrimitiveCount; uint NormalCone; float4 BoundingSphere; uint MaterialId; // hi-16bit : ShaderId, low-16bit : MaterialId. }; ConstantBuffer g_Constants : register(b0); StructuredBuffer g_Meshlets : register(t0); Texture2D g_VisibilityBuffer : register(t1); RWStructuredBuffer g_ShaderIdCounter : register(u0); RWStructuredBuffer g_WorkList : register(u1); RWStructuredBuffer g_WorkListCounter : register(u2); RWTexture2D g_RenderTarget : register(u3);

Slide 197

Slide 197 text

201 struct Constants { uint MaxShaderCount; uint3 Reserved; }; ConstantBuffer g_Constants : register(b0); RWStructuredBuffer g_ShaderIdCounter : register(u0); RWStructuredBuffer g_ShaderIdOffset : register(u1); [numthreads(32, 1, 1)] void main(uint3 dispatchId : SV_DispatchThreadID, uint3 groupThreadId : SV_GroupThreadID) { const uint maxShaderCount = min(g_Constants.MaxShaderCount, MAX_SHADERS); for(uint i=0; i= maxShaderCount) break; uint offset = WavePrefixSum(g_ShaderIdCounter[index]); g_ShaderIdOffset [index] = offset; g_ShaderIdCounter[index] = 0; // カウンターを再使用するためにリセット. } }

Slide 198

Slide 198 text

202 [numthreads(32, 1, 1)] void main(uint3 dispatchId : SV_DispatchThreadID) { const uint maxShaderCount = min(g_Constants.MaxShaderCount, MAX_SHADERS); // ワークリスト数を取得. uint workListCounter = 0; if (WaveIsFirstLane()) { workListCounter = g_WorkListCounter[0]; } workListCounter = WaveReadLaneFirst(workListCounter); // ワークリスト数を超えていたら終了. if (dispatchId.x >= workListCounter) { return; } // ソート前データを取得. WorkItem input = g_InputWorkList[dispatchId.x]; const uint shaderId = input.ShaderId; // 書き込み場所を取得. uint index = 0; InterlockedAdd(g_ShaderIdCounter[shaderId], 1, index); // グローバルオフセットを足しこむ. index += g_ShaderIdOffset[shaderId]; // ソート後データを書き込み. g_OutputWorkList[index] = input.FlatPixelId; } struct WorkItem { uint ShaderId; uint FlatPixelId; }; struct Constants { uint MaxShaderCount; uint3 Reserved; }; ConstantBuffer g_Constants : register(b0); StructuredBuffer g_InputWorkList : register(t0); StructuredBuffer g_WorkListCounter : register(t1); RWStructuredBuffer g_ShaderIdCounter : register(u0); RWStructuredBuffer g_ShaderIdOffset : register(u1); RWStructuredBuffer g_OutputWorkList : register(u2);

Slide 199

Slide 199 text

• 下図のようにナイーブな実装だと結構重たい • 特にInterlockedAdd()を実行箇所が重たいことが判明 203 BuildWorkList SortWorkList 0.11ms 0.08ms

Slide 200

Slide 200 text

• Wave Intrinsicsを用いて最適化を行ったところ約2倍程度高速化 (0.19ms → 0.08ms) 204 BuildWorkList SortWorkList 0.11ms → 0.06ms 0.08ms → 0.02ms

Slide 201

Slide 201 text

• Shader Model 6.5から利用可能なWaveMatch()とWaveMultiPrefixCountBits()を用いて, 書き込み回数の抑制を行った • WaveMatch()を使用して,自分と同じシェーダを持つレーンをビットマスクから判別 • countbits()を用いて同じシェーダを持つレーン数を求める • 書き込みインデックス算出にはWaveMultiPrefixCountBits()を使用する • カウンター数の加算は,ビットマスクが一番小さい所で1回だけ書き込む 205 uint shaderId = 3; uint4 mask = WaveMatch(shaderId);// 01000101 uint4 counts = countbits(mask); uint shaderCount = counts.x + counts.y + counts.z + counts.w; // 3 if (WaveGetLaneIndex() == lowestLane) // 最初に登場するレーンで書き込み. { InterlockedAdd(g_ShaderIdOffset[shaderId], shaderCount); }

Slide 202

Slide 202 text

206 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 ShaderId=2 0 0 0 0 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 0 0 0 0 0 6

Slide 203

Slide 203 text

207 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 ShaderId=2 1 0 0 0 1 0 0 0 0 0 0 0 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 0 0 0 0 0 6

Slide 204

Slide 204 text

208 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 ShaderId=2 1 0 0 0 1 0 0 0 2 0 0 0 0 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 0 0 0 0 0 6

Slide 205

Slide 205 text

209 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 ShaderId=2 1 0 0 0 1 0 0 0 2 0 0 0 0 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 0 0 0 0 0 6

Slide 206

Slide 206 text

210 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 ShaderId=2 1 0 0 0 1 0 0 0 2 2 0 0 0 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 0 0 0 0 0 6

Slide 207

Slide 207 text

211 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 ShaderId=2 1 0 0 0 1 0 0 0 2 ShaderId=3 0 1 0 0 0 1 0 1 3 2 3 0 0 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 0 0 0 0 0 6

Slide 208

Slide 208 text

212 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 ShaderId=2 1 0 0 0 1 0 0 0 2 ShaderId=3 0 1 0 0 0 1 0 1 3 ShaderId=4 0 0 1 0 0 0 1 0 2 2 3 2 0 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 0 0 0 0 0 6

Slide 209

Slide 209 text

213 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 ShaderId=2 1 0 0 0 1 0 0 0 2 ShaderId=3 0 1 0 0 0 1 0 1 3 ShaderId=4 0 0 1 0 0 0 1 0 2 ShaderId=5 0 0 0 1 0 0 0 0 1 2 3 2 1 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 0 0 0 0 0 6

Slide 210

Slide 210 text

214 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 ShaderId=2 1 0 0 0 1 0 0 0 2 ShaderId=3 0 1 0 0 0 1 0 1 3 ShaderId=4 0 0 1 0 0 0 1 0 2 ShaderId=5 0 0 0 1 0 0 0 0 1 2 3 2 1 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 2 0 0 0 0 6

Slide 211

Slide 211 text

215 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 ShaderId=2 1 0 0 0 1 0 0 0 2 ShaderId=3 0 1 0 0 0 1 0 1 3 ShaderId=4 0 0 1 0 0 0 1 0 2 ShaderId=5 0 0 0 1 0 0 0 0 1 2 3 2 1 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 2 0 0 0 0 6

Slide 212

Slide 212 text

216 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 ShaderId=2 1 0 0 0 1 0 0 0 2 ShaderId=3 0 1 0 0 0 1 0 1 3 ShaderId=4 0 0 1 0 0 0 1 0 2 ShaderId=5 0 0 0 1 0 0 0 0 1 2 3 2 1 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 2 0 0 0 0 6

Slide 213

Slide 213 text

217 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 ShaderId=2 1 0 0 0 1 0 0 0 2 ShaderId=3 0 1 0 0 0 1 0 1 3 ShaderId=4 0 0 1 0 0 0 1 0 2 ShaderId=5 0 0 0 1 0 0 0 0 1 0 3 2 1 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 2 5 0 0 0 6

Slide 214

Slide 214 text

218 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 ShaderId=2 1 0 0 0 1 0 0 0 2 ShaderId=3 0 1 0 0 0 1 0 1 3 ShaderId=4 0 0 1 0 0 0 1 0 2 ShaderId=5 0 0 0 1 0 0 0 0 1 0 0 2 1 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 2 5 7 0 0 6

Slide 215

Slide 215 text

219 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 ShaderId=2 1 0 0 0 1 0 0 0 2 ShaderId=3 0 1 0 0 0 1 0 1 3 ShaderId=4 0 0 1 0 0 0 1 0 2 ShaderId=5 0 0 0 1 0 0 0 0 1 0 0 0 1 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 2 5 7 8 0 6

Slide 216

Slide 216 text

220 PixelId : 0 ShaderId : 2 PixelId : 1 ShaderId : 3 PixelId : 2 ShaderId : 4 PixelId : 3 ShaderId : 5 PixelId : 4 ShaderId : 2 PixelId : 5 ShaderId : 3 PixelId : 7 ShaderId : 3 PixelId : 6 ShaderId : 4 ShaderId=2 1 0 0 0 1 0 0 0 2 ShaderId=3 0 1 0 0 0 1 0 1 3 ShaderId=4 0 0 1 0 0 0 1 0 2 ShaderId=5 0 0 0 1 0 0 0 0 1 0 0 0 0 Id Counter 0 0 0 1 2 3 4 5 Id Offset 0 Index 0 0 2 5 7 8 0 6

Slide 217

Slide 217 text

221 #if ENABLE_OPTIMIZATION // 自分と同じシェーダを持っているレーンを調べて,シェーダ数を求める. uint4 mask = WaveMatch(shaderId); uint count = SumCountBits(mask); // 自分が最初だったら,シェーダ数を書き込む. uint lowLane = GetLowestLane(mask); if (WaveGetLaneIndex() == lowLane) { InterlockedAdd(g_ShaderIdCounter[shaderId], count); } uint index = 0; uint workCount = WaveActiveCountBits(true); if (WaveIsFirstLane()) { InterlockedAdd(g_WorkListCounter[0], workCount, index); } index = WaveReadLaneFirst(index); index += WavePrefixCountBits(true); #else // シェーダ数を数える. InterlockedAdd(g_ShaderIdCounter[shaderId], 1); // 書き込み番号を取得. int index = 0; InterlockedAdd(g_WorkListCounter[0], 1, index); #endif #if ENABLE_OPTIMIZATION // 自分と同じシェーダを持っているレーンを調べて,ローカル番号を求める. uint4 mask = WaveMatch(input.ShaderId); uint index = WaveMultiPrefixCountBits(true, mask); // シェーダ数を求める. uint count = SumCountBits(mask); uint lowLane = GetLowestLane(mask); // 自分が最初だったらシェーダ数を書き込み,ローカルオフセットを取得. uint localOffset = 0; uint globalOffset = 0; if (WaveGetLaneIndex() == lowLane) { InterlockedAdd(g_ShaderIdCounter[input.ShaderId], count, localOffset); globalOffset = g_ShaderIdOffset [input.ShaderId]; } // 同じシェーダを持つレーンで共有出来るようにする. localOffset = WaveReadLaneAt(localOffset, lowLane); globalOffset = WaveReadLaneAt(globalOffset, lowLane); // ローカルオフセットを足しこむ. index += localOffset; // グローバルオフセットを足しこむ. index += globalOffset; #else const uint shaderId = input.ShaderId; // 書き込み場所を取得. uint index = 0; InterlockedAdd(g_ShaderIdCounter[shaderId], 1, index); // グローバルオフセットを足しこむ. index += g_ShaderIdOffset[shaderId]; #endif // ビットを数え上げて、水平加算する. uint SumCountBits(uint4 mask) { uint4 count = countbits(mask); return dot(count, 1u.xxxx); } // レーンビットマスクの中から最も低いレーン番号を求める uint GetLowestLane(uint4 mask) { uint4 lowLanes = (uint4)(firstbitlow(mask) | uint4(0, 32, 64, 96)); return min(min(lowLanes.x, lowLanes.y), min(lowLanes.z, lowLanes.w)); }

Slide 218

Slide 218 text

• そもそも実装効率が良くない箇所が多々あるので、さらなる改善は今後の課題 222 Image credits: Sue Seecof, https://www.flickr.com/photos/126344637@N05/26013859361, licensed under https://creativecommons.org/licenses/by/2.0/

Slide 219

Slide 219 text

223

Slide 220

Slide 220 text

224

Slide 221

Slide 221 text

• 作成したワークリストを用いて, シェーダごとに間接引数(IndirectArgument)を 作成し,ExecuteIndirect()によりコンピュート シェーダを起動 • 重心座標を求めて,各頂点データを補間し, 各ピクセル位置においてシェーディングに 必要なデータを求める • シェーディング用のデータが求められたら あとは従来通りにシェーディングを行う 225

Slide 222

Slide 222 text

• ワークリスト作成時に各シェーダに属するピクセル数をカウントアップ済み • GPU上で数を数えているので,データはGPU上にあってCPU上にない • そのため,GPU上でドローコールの引数(Indirect Argument)を作成 • 作成したドローコール引数が格納されているバッファをExecuteIndirect()の引数に設定して, コンピュートシェーダによるシェーディング処理を実行 226 [numthreads(32, 1, 1)] void main(uint3 dispatchId : SV_DispatchThreadID) { const uint maxShaderCount = min(g_Constants.MaxShaderCount, MAX_SHADERS); if (dispatchId.x >= maxShaderCount) return; DispatchArgument arg; arg.ThreadX = g_ShaderCounter[dispatchId.x]; arg.ThreadY = 1; arg.ThreadZ = 1; g_IndirectArgs[dispatchId.x] = arg; }

Slide 223

Slide 223 text

• V-Bufferに格納されたインスタンスIDを使用して,ワールド行列を取得 • プリミティブIDを使用して,3頂点のデータを取得 227 uint shaderOffset = g_ShaderIdOffset[g_Constants.ShaderId]; uint flatPixelId = g_WorkList[shaderOffset + dispatchId.x]; // ピクセル座標を算出. uint2 pixelId; pixelId.x = flatPixelId & 0xFFFF; pixelId.y = (flatPixelId >> 16) & 0xFFFF; if (any(pixelId > g_Constants.RenderTargetSize)) return; // ビジビリティバッファ取得. uint2 visibility = g_VisibilityBuffer[pixelId]; if (visibility.y == 0) return; // IDを復元. uint instanceId = ((visibility.x >> 8) & 0xFFFFFF); uint primitiveId = (visibility.x & 0xFF); uint meshletId = visibility.y - 1; // 1始まりなので,0始まりにするためにマイナス1. // インスタンスデータ取得. MeshInstanceParam instance = g_MeshInstances[instanceId]; // メッシュレット情報取得. MeshletInfo meshlet = g_Meshlets[meshletId]; // プリミティブ番号を取得. uint3 tris = GetPrimitiveIndex(primitiveId + meshlet.PrimitiveOffset); // 頂点インデックスを取得. uint3 idx = uint3( g_VertexIndices[tris.x + meshlet.VertexOffset], g_VertexIndices[tris.y + meshlet.VertexOffset], g_VertexIndices[tris.z + meshlet.VertexOffset]);

Slide 224

Slide 224 text

• マテリアル評価するために,重心座標を求める • カメラからスクリーンの各ピクセルを目標にレイを飛ばして求める 228 float3 intersect(float3 p0, float3 p1, float3 p2, float3 rayOrigin, float3 rayDir) { float3 eo = rayOrigin – p0; float3 e1 = p1 – p0; float3 e2 = p2 – p0; float3 r = cross(rayDir, e2); float3 s = cross(eo, e1); float iV = 1.0f / dot(r, e1); float V1 = dot(r, e0); float V2 = dot(s, rayDir); float b = V1 * iV; float c = V2 * iV; float a = 1.0f – b- c; return float3(a, b, c); } rayOrigin = cameraPosition; rayDir = normalize( cameraAxisX * (pixelCoord.x + 0.5f) + cameraAxisY * ((Height – pixelCoord.y – 1) + 0.5f) + cameraAxisZ);

Slide 225

Slide 225 text

• 従来はポリゴンを描画する際にGPUが適切なミップレベルを算出して行ってくれていた • V-Bufferはもうポリゴンを書いてしまった後に,テクスチャをフェッチするので, このハードウェア機能が使用できない • そのため,SampleGrad()を使用して適切なミップレベルが選択されるように実装 • SampleGrad()には偏微分値(ddx, ddy)が必要となるため,計算で求める [Hable 2021] 229

Slide 226

Slide 226 text

230 BarycentricDeriv CalcFullBary(float4 pt0, float4 pt1, float4 pt2, float2 pixelNdc, float2 winSize) { BarycentricDeriv ret = (BarycentricDeriv)0; float3 invW = rcp(float3(pt0.w, pt1.w, pt2.w)); float2 ndc0 = pt0.xy * invW.x; float2 ndc1 = pt1.xy * invW.y; float2 ndc2 = pt2.xy * invW.z; float invDet = rcp(determinant(float2x2(ndc2 - ndc1, ndc0 - ndc1))); ret.m_ddx = float3(ndc1.y - ndc2.y, ndc2.y - ndc0.y, ndc0.y - ndc1.y) * invDet * invW; ret.m_ddy = float3(ndc2.x - ndc1.x, ndc0.x - ndc2.x, ndc1.x - ndc0.x) * invDet * invW; float ddxSum = dot(ret.m_ddx, float3(1,1,1)); float ddySum = dot(ret.m_ddy, float3(1,1,1)); float2 deltaVec = pixelNdc - ndc0; float interpInvW = invW.x + deltaVec.x*ddxSum + deltaVec.y*ddySum; float interpW = rcp(interpInvW); ret.m_lambda.x = interpW * (invW[0] + deltaVec.x*ret.m_ddx.x + deltaVec.y*ret.m_ddy.x); ret.m_lambda.y = interpW * (0.0f + deltaVec.x*ret.m_ddx.y + deltaVec.y*ret.m_ddy.y); ret.m_lambda.z = interpW * (0.0f + deltaVec.x*ret.m_ddx.z + deltaVec.y*ret.m_ddy.z); ret.m_ddx *= (2.0f/winSize.x); ret.m_ddy *= (2.0f/winSize.y); ddxSum *= (2.0f/winSize.x); ddySum *= (2.0f/winSize.y); ret.m_ddy *= -1.0f; ddySum *= -1.0f; float interpW_ddx = 1.0f / (interpInvW + ddxSum); float interpW_ddy = 1.0f / (interpInvW + ddySum); ret.m_ddx = interpW_ddx*(ret.m_lambda*interpInvW + ret.m_ddx) - ret.m_lambda; ret.m_ddy = interpW_ddy*(ret.m_lambda*interpInvW + ret.m_ddy) - ret.m_lambda; return ret; } struct BarycentricDeriv { float3 m_lambda; float3 m_ddx; float3 m_ddy; };

Slide 227

Slide 227 text

231 // ワールド空間位置を取得. float4 worldPos[3]; worldPos[0] = mul(instance.CurrWorld, float4(g_Positions[idx.x], 1.0f)); worldPos[1] = mul(instance.CurrWorld, float4(g_Positions[idx.y], 1.0f)); worldPos[2] = mul(instance.CurrWorld, float4(g_Positions[idx.z], 1.0f)); float4 viewPos[3]; viewPos[0] = mul(g_ViewParam.View, worldPos[0]); viewPos[1] = mul(g_ViewParam.View, worldPos[1]); viewPos[2] = mul(g_ViewParam.View, worldPos[2]); float4 projPos[3]; projPos[0] = mul(g_ViewParam.Proj, viewPos[0]); projPos[1] = mul(g_ViewParam.Proj, viewPos[1]); projPos[2] = mul(g_ViewParam.Proj, viewPos[2]); float2 pixelNdc = ((pixelId + 0.5f.xx) / (float2)g_Constants.RenderTargetSize) * float2(2.0f, -2.0f) - float2(1.0f, -1.0f); // 重心座標を求める. BarycentricsParam baryParam = CalcFullBary(projPos[0], projPos[1], projPos[2], pixelNdc, g_Constants.RenderTargetSize); float3 worldNormal[3]; worldNormal[0] = normalize(mul((float3x3)instance.CurrWorld, g_Normals[idx.x])); worldNormal[1] = normalize(mul((float3x3)instance.CurrWorld, g_Normals[idx.y])); worldNormal[2] = normalize(mul((float3x3)instance.CurrWorld, g_Normals[idx.z])); float2 uv[3]; uv[0] = g_TexCoords[idx.x]; uv[1] = g_TexCoords[idx.y]; uv[2] = g_TexCoords[idx.z]; // 補間した頂点データを求める. ShadingParam param; { BaryInterpolate3(baryParam, worldPos[0].xyz, worldPos[1].xyz, worldPos[2].xyz, param.WorldPos); BaryInterpolate3(baryParam, worldNormal[0], worldNormal[1], worldNormal[2], param.Normal); param.Normal = normalize(param.Normal); param.MaterialId = meshlet.MaterialId; BaryInterpolate2WithDeriv(baryParam, uv[0], uv[1], uv[2], param.TexCoord, param.DxTexCoord, param.DyTexCoord); } // シェーディング処理. float4 color = Shading(param); // レンダーターゲットに書き込み. g_RenderTarget[pixelId] = color;

Slide 228

Slide 228 text

232

Slide 229

Slide 229 text

233

Slide 230

Slide 230 text

234

Slide 231

Slide 231 text

• • • • • • • 235

Slide 232

Slide 232 text

• • • • • • • 236

Slide 233

Slide 233 text

最後におさらい 237

Slide 234

Slide 234 text

• • • • • • • • • • • 238

Slide 235

Slide 235 text

発表中に説明できなかったもの & おまけ 239

Slide 236

Slide 236 text

見えるものだけを描画する手法 240

Slide 237

Slide 237 text

• その結果、ポリゴンがいっぱいなので処理が重くなる • 視認出来るポリゴンは描画したい • でも、ゲーム重くなるのは避けたい • じゃあ、必要のないポリゴンを削除すればよいのでは? 241

Slide 238

Slide 238 text

• 視錐台カリングで明らかにカメラに映らないポリゴンはもう消した • ピクセルにならない(寄与しない)ポリゴンも消した • でも、更に削って処理を速くしたい! • カメラ内で映っているオブジェクトは消したくない • 逆に言うと,カメラ内でも映らないものがあるなら,描画しなくていいはず! 242

Slide 239

Slide 239 text

• カメラ内で,オブジェクト背後の不可視オブジェクトをカリングする手法 • メッシュをフル描画するのは高コストになるため,AABBなどの簡易なオブジェクトを描画し, そのオブジェクトが描画されたかどうかを判定することが多い 243

Slide 240

Slide 240 text

• Unityにおける大量オブジェクトのレンダリング高速化事例 〜GPU駆動レンダリング & Hi-Zカリングの統合〜, CEDEC 2024. • 『GRANBLUE FANTASY: Relink』ソフトウェアラスタライザによる実践的なオクルージョンカリング, CEDEC 2024. • 自動生成マップの描画を高速化!~ComputeShaderを使ったオクルージョンカリングの理論と実装~, CEDEC 2023. • Practical technologies to create Big World city with time-of-day/昼夜の変化のある「ビッグワールド」の町の実現のための実用 的な技術の紹介, CEDEC 2023. • 内製レンダリングエンジンにおける採用技術と最適化手法, CEDEC 2019. • 最新タイトルのグラフィックス最適化事例, CEDEC 2018. • Umbra Software:次世代に向けたレンダリングと可視性の最適化について, CEDEC 2012. • 更に進化した遮蔽カリングシステムUmbra3の実力,CEDEC 2011. …などなど 244

Slide 241

Slide 241 text

• [Pohlmann 2021] “Samurai Landscapes: Building and Rendering Tsushima Island on PS4”, 1. 前フレームの深度を1/4解像度のターゲットにダウンサンプルする 2. 現在のビュー空間にリプロジェクションする 3. エピポール検索+ヒューリスティックを用いてリプロジェクションすることにより残った穴埋めをする 4. max-depthのミップチェーンを生成する 245

Slide 242

Slide 242 text

246 • 3次元空間を異なる位置のカメラから撮影した幾何 • カメラ𝑂𝐿 から見えるスクリーン上の𝑋𝐿 は複数の候補が考えられる(例えば,𝑋1 , 𝑋2 , 𝑋3 など) • 1つのカメラ上のみでは,3D上の位置を特定不可 • しかし,カメラ𝑂𝑅 から同一物体が𝑋𝑅 上に表示されていることが分かった場合には, 𝑋が3D上の位置であると特定可能

Slide 243

Slide 243 text

• 現在フレームのカメラと,1フレーム前のカメラの情報を用いて,エピポーラ探索を行う • エピポーラ検索を用いて見つけた深度値で穴を埋める • 見つからなかった場合はHi-Zの粗いミップレベルを参照するようにし,検索を固定回数に 247

Slide 244

Slide 244 text

248

Slide 245

Slide 245 text

249

Slide 246

Slide 246 text

250 [numthreads(8, 8, 1)] void main(uint3 dispatchId : SV_DispatchThreadID) { if (any(dispatchId.xy >= g_Constants.RenderTargetSize)) return; // 前フレームの深度を取得. float zPrev = g_PrevDepthMap[dispatchId.xy]; // 前フレームのクリップ空間. float4 posClipPrev = float4(ToClipPos(dispatchId.xy), zPrev, 1.0f); // 現在フレームのクリップ空間に変換する. float4 posClip = mul(g_TransParam.PrevClipToCurrClip, posClipPrev); posClip.xyz /= posClip.w; // Perspective Divide. // テクスチャ座標に変換する. float2 uv = posClip.xy * 0.5f + 0.5f.xx; // テクスチャ座標が適切かどうかチェック. if (all(uv >= 0.0f) && all(uv <= 1.0f)) { // Z値. float z = posClip.w; // ニア平面より手前ならクランプ. if (z <= g_TransParam.Current.NearClip) z = g_TransParam.Current.NearClip; // 深度バイアスを考慮. z += g_Constants.DepthBias; // ファー平面よりも奥ならクランプ. if (z > g_TransParam.Current.FarClip || zPrev >= g_TransParam.Previous.FarClip) z = g_TransParam.Current.FarClip; // ピクセル位置を取得. float2 xy = floor(uv * g_Constants.RenderTargetSize); // リプロジェクションしたZ値を書き込む. CurrDepthAtomicMax(xy, z); } // 範囲外の処理. { // 現在フレームのファー平面(Reverse-Zを想定)のクリップ空間位置. float4 posClip = float4(ToClipPos(dispatchId.xy), 0.0f, 1.0f); // 前フレームのクリップ空間位置に変換. float4 posClipPrev = mul(g_TransParam.CurrClipToPrevClip, posClip); // 画面外であることをチェック. if (any(abs(posClipPrev.xy) >= abs(posClipPrev.w))) { float2 uv = (posClipPrev.xy / posClipPrev.w) * 0.5f + 0.5f.xx; float z = g_PrevDepthMap.SampleLevel(LinearClamp, uv, 0.0f) + g_Constants.DepthBias; CurrDepthAtomicMax(dispatchId.xy, z); } } }

Slide 247

Slide 247 text

251 [numthreads(8, 8, 1)] void main(uint3 dispatchId : SV_DispatchThreadID) { // エピポールを求める. float3 epipole = normalize(g_TransParam.Previous.CameraPos - g_TransParam.Current.CameraPos); // 射影空間でのエピポールを求める. float4 clipEpipole = mul(g_TransParam.Current.Proj, float4(epipole, 0.0f)); // UV空間でのエピポールを求める. float2 uvEpipole = normalize(clipEpipole.xy * 0.5f + 0.5f.xx); // テクスチャ座標を求める. float2 uv = (dispatchId.xy + 0.5f.xx) * g_TransParam.RenderTargetSize.zw; // 深度を取得. float zMax = g_SrcDepth.SampleLevel(LinearClamp, uv, 0.0f); // 穴埋めされていない箇所であることをチェック. if (zMax == -FLT_MAX) { // UV空間での検索方向を求める. float2 normalEpipole = normalize(uvEpipole * uv); // 検索の刻み幅を求める. float2 dUvStep = normalEpipole * g_TransParam.RenderTargetSize.zw; // エピポーラ検索を行う. for(uint mip=1; mip <= g_Constants.MipLevels; ++mip) { // 検索点を求める. float2 uvSearch = uv + dUvStep; // 検索点における深度を取得 float z = g_SrcDepth.SampleLevel(LinearClamp, uvSearch, mip); // 次のミップを調べるために,2倍する(テクセルサイズが2倍になるため). dUvStep *= 2.0f; // 深度が適切であるかどうかチェック. if (z != -FLT_MAX) { z = max(z, g_DstDepth[dispatchId.xy]); z += g_Constants.DepthBias; z = min(z, g_TransParam.Current.FarClip); // 正常終了. return; } } // 適切な深度が取得できない場合は,背景として遮蔽されない扱いにする. if (zMax < g_TransParam.Current.NearClip) zMax = g_TransParam.Current.FarClip; // ファー平面でクランプ. zMax = min(zMax, g_TransParam.Current.FarClip); // 検索に引っかからなかった場合のフォールバックとして書き込み. g_DstDepth[dispatchId.xy] = zMax; } }

Slide 248

Slide 248 text

• [Pohlmann 2021]では,GPU Sceneが2ms程度改善している。 • Reproject/Fill Holes/Gen Mipsは非同期コンピュートで実行できるため,この処理にかかる時間は隠蔽可能。 252

Slide 249

Slide 249 text

• これはゲームまたはシーンによってケースバイケースで異なる • CPUが余っているならGPUオクルージョンカリング, GPUが余っているならCPUオクルージョンカリングがおススメ まずは、組み込み前に計測してどちらが適切かを判断しましょう • エンジンとして実装する場合は,実装期間の余裕があるなら両方用意するのが得策 253

Slide 250

Slide 250 text

ビット数を減らす 254

Slide 251

Slide 251 text

• インデックスバッファを圧縮する手法 • 三角形ストリップ化し,L/Rフラグと増加フラグを持ち表現 • シェーダは1bitデータで持てないため,メッシュレット単位でまとめてデータ を持つことにより,シェーダでアクセスできる形で保持する 255

Slide 252

Slide 252 text

• [Oberberger2024; Kuth2024; AMD2024] 256 0 1 2 2 3 1 2 3 4 2 4 5 5 2 0 0 0 0 0 5 6 6 7 7 8 8 8 9 1 1 32 bit Index 32 bit Index 32 bit Index 96 bit per Triangles

Slide 253

Slide 253 text

• [Oberberger2024; Kuth2024; AMD2024] 257 0 1 2 2 3 1 2 3 4 2 4 5 5 2 0 0 0 0 0 5 6 6 7 7 8 8 8 9 1 1 8 bit Index 8 bit Index 8 bit Index 24 bit per Triangles

Slide 254

Slide 254 text

• [Oberberger2024; Kuth2024; AMD2024] 258 0 1 2 3 4 5 6 7 8 9 R R L L L L L L R 8 bit Index 1 bit Code 9 bit per Triangles

Slide 255

Slide 255 text

• [Oberberger2024; Kuth2024; AMD2024] 259 0 1 2 3 4 5 6 7 8 9 R R L L L L L L R 0 1

Slide 256

Slide 256 text

• [Oberberger2024; Kuth2024; AMD2024] 260 0 1 2 + + + + + + + R R L L L L L L R 0 1 1 bit Code 1 bit Code 8 bit Index

Slide 257

Slide 257 text

• [Oberberger2024; Kuth2024; AMD2024] 261 0 1 2 + + + + + + + R R L L L L L L R 0 1 1 bit Code 1 bit Code 3 bit Index 5 bit per Triangles

Slide 258

Slide 258 text

• 三角形ストリップ化したインデックスバッファを準備 262 0 2 3 4 5 6 7 8 9 1

Slide 259

Slide 259 text

• 三角形ストリップ化したインデックスバッファを準備 263 0 2 3 4 5 6 7 8 9 1

Slide 260

Slide 260 text

264

Slide 261

Slide 261 text

265

Slide 262

Slide 262 text

• 赤線で塗られた7番目の三角形(0-7-8)を復元することを考えます。(7番目のL/RフラグはLなので,インデックスは7番目を参照し,8と確定) • 三角形ストリップなので,直前の2つを調べます。 • L/Rフラグは1つ前がL, 2つ前がR。Lフラグ(=連続)なのでインデックスバッファは1つ前が7と確定。(この時点で8と7が確定) • Rフラグに当たる箇所の求め方を知る必要があります。どこからRフラグに変化したかを遡って調べます。 これを調べるためには,L/Rフラグはビット化されているので,どこでビットが変わったかを調べれば良いことになります。 • L→Rに切り替わるのは,4番目→5番目と分かるので,インデックスバッファの4番目を参照します。 • インデックバッファの4番目が0であり,これで3つ揃ったので(0-7-8)と確定します。実装コードについては[AMD 2024]を参照。 (インデックス番号が小さいに順にすると0-7-8となり,インデックスバッファが復元できます) 266 1 0 0 0 1 0 0 0 1 0 1 1 1 0 0 0 0 0

Slide 263

Slide 263 text

267

Slide 264

Slide 264 text

• 基本的な考え方は,Reuse Bufferを使わない場合と同様。 • Reuse Bufferを使って,インデックスを復元する。 • Rフラグの数とReuse Bufferの数は一致するはずなので,自分の位置以下のRフラグの数を数えれば, 参照するReuse Bufferの番号が求まるはずなので,ビットの数え上げ関数を用いて実装すれば良い。 • 実装コードについては[AMD 2024]を参照。 268

Slide 265

Slide 265 text

269

Slide 266

Slide 266 text

270

Slide 267

Slide 267 text

• [Okuda 2019] 奥田雅史, 川名勇気, 落合仁美子, 二階堂将也, “『描画が出来る人』ってどうやって育てればいいんだろう?~描画エンジニア育成プロジェクトポストモーテム~”, CEDEC 2019 • [Wihlidal 2016] Grahm Wihlidal, “Optimizing the Graphics Pipeline with Compute”, GDC 2016. • [Uralsky 2019] Yury Uralsky, “MESH SHADING: Towards greater efficiency in geometry processing”, SIGGRAPH 2019 Courses: Advances in Real-Time Rendering in Games. • [Karis 2021] Brian Karis, Rune Stubbe, Graham Wihlidal, “A Deep Dive into Nanite Virtualized Geometry”, SIGGRAPH 2021 Courses: Advances in Real-Time Rendering in Games. • [Jansson 2024] Erik Jansson, “GPU-driven Rendering with Mesh Shaders in Alan Wake2”, Digital Dragons 2024. • [Lopez 2025] Nicolas Lopez, “Rendering ‘Assassin’s Creed Shadows’”, GDC 2025. • [Mishima 2025] Hitoshi Mishima, “RE ENGINE Meshlet Rendering Pipeline”, Rendering Engine Architecture Conference 2025. • [Microsoft 2021] Microsoft, “DirectX-Specs : HLSL Wave Size”, https://microsoft.github.io/DirectX-Specs/d3d/HLSL_SM_6_6_WaveSize.html, 2021. • [Microsoft 2023] Microsoft, “DirectX-Specs : Mesh Shader”, https://microsoft.github.io/DirectX-Specs/d3d/MeshShader.html, 2023. • [Microsoft 2024] Microsoft, “DirectXShaderCompile Wave Intrinsics”, https://github.com/microsoft/DirectXShaderCompiler/wiki/Wave-Intrinsics, 2024 • [shikihuiku 2020] shikihuiku, “HLSLのWave Intrinsicsについて”, https://shikihuiku.github.io/post/wave_intrinsics1/, 2020. • [Sreckovic 2024] , “Compute shader wave intrinsics tricks”, https://medium.com/@marehtcone/compute-shader-wave-intrinsics-tricks-e237ffb159ef, 2024. • [Honda 2019] 本多圭, “フラスタムカリング入門、良いフラスタムの作り方”, CEDEC 2019. • [Mishima 2018] 三嶋仁, “最新タイトルのグラフィックス最適化事例”, CEDEC 2018. • [Pohlmann 2021] Matthew Pohlmann, “Samurai Landscapes: Building and Rendering Tsushima Island on PS4”, https://gdcvault.com/play/1027352/Samurai- Landscapes-Building-and-Rendering, GDC 2021. 271

Slide 268

Slide 268 text

• [Hable 2021] John Hable, “Visibility Buffer Rendering with Material Graphs”, http://filmicworlds.com/blog/visibility-buffer-rendering-with-material-graphs/, 2021. • [Burns 2013] Christopher A. Burns, Warren A. Hunt, “The Visibility Buffer: A Cache-Friendly Approach to Deferred Shading”, The Journal of Computer Graphics Techniques, vol.2, no.2, pp.55-69, 2013. • [stack overflow 2017], stack overflow, “Radius of projected sphere in screen space”, https://stackoverflow.com/questions/21648630/radius-of-projected- sphere-in-screen-space, 2017. • [Garland 1997] Michael Garland, Paul S. Heckbert, “Surface simplification using quadric error metrics”, SIGGRAPH 97, pp.208-216, August 1997. • [Nam 2025] キュウ キャル, 南 相培, 佐光 一輝, “モバイルにも使える軽量な構造を持つ仮想化ジオメトリシステムの設計と実装について”, CEDEC 2025. • [Kuth 2024] Bastian Kuth, Max Oberberger, Felix Kawala, Sander Reitter, Sebastian Michel, Matthaus Chadas, Quirin Meyer, “Towards Practical Meshlet Compression”, 2024. • [AMD 2024] AMD, “GPU Open : Meshlet compression”, https://gpuopen.com/learn/mesh_shaders/mesh_shaders-meshlet_compression, 2024. • [Cigolle 2014] Zina H. Cigolle, San Donow, Daniel Evangelakos, Michael Mara, Morgan McGuire, Quirin Meyer, “A Survey of Efficient Representations for Independent Unit Vectors”, Journal of Computer Graphics Techniques, Vol.3, No.2, pp.1-30, 2014. • [John White 3D 2017] John White 3D, “Signed Octahedron Normal Encoding”, https://johnwhite3d.blogspot.com/, 2017. • [Schüler2007] Christian Schüler, “Normal Mapping without Precomputed Tangents”, ShaderX5, Chapter 2.6, pp.131-140, 2007. • [Schüler2013] Christian Schüler, “Followup: Normal Mapping Without Precomputed Tangents”, http://www.thetenthplanet.de/archives/1180, 2013. • [Geffroy 2020] Jean Geffroy, Axel Gneiting, Yixin Wang, “Rendering the Hellscape of Doom Eternal”, SIGGRAPH 2020 Advances in Real-Time Rendering course, 2020. • [Ong 2023] Jeremy Ong, “Tangent Spaces and Diamon Encoding”, https://www.jeremyong.com/graphics/2023/01/09/tangent-spaces-and-diamond-encoding/, 2023. • [Mclaren 2022] James Mclaren, “Adventures with Deferred Texturing in Horizon Forbidden West”, GDC 2022. 272

Slide 269

Slide 269 text

• [Haar 2015] Ulrich Haar, Sebastian Aaltonen, “GPU-Driven Rendering Pipelines”, SIGGRAPH 2015: Advances in Real-Time Rendering in Games, 2015. • [Takeshige 2018] 竹重 雅也, “DirectX Raytracing – The life of a ray tracing kernel”, CEDEC 2018. • [Akuzawa 2024] 阿久澤 陽菜, ルフェ マキシム, “Mesh shaderを活用したスキニングメッシュに対するサブディビジョンサーフェイス”, CEDEC 2024. • [Kapoulkine 2025] Arseny Kapoulkine, “meshoptimizer”, https://github.com/zeux/meshoptimizer, 2025. • [Wikipedia 2025a] Wikipedia, “Epipolar geometry, “, https://en.wikipedia.org/wiki/Epipolar_geometry, 2025 • [Wikipedia 2025b] Wikipedia, “Spherical cap”, https://en.wikipedia.org/wiki/Spherical_cap, 2025. • [Valient 2007] Michal Valient, “Deferred Rendering in Killzone 2”, GDC 2007. • [Legarde 2014] Sebastien Lagarde, Charles de Rousiers, “Moving Frostbite to Physically Based Rendering 3.0”, SIGGRAPH 2014 Course: Physically Based Shading in Theory and Practice, 2014. • [Engel 2016] Wolfgang Engel, “The filter and Culled Visibility Buffer”, GDC Europe 2016. • [Anagnostou 2018] Kostas Anagnostou, “GPU driven rendering experiments”, Digital Dragons 2018. • [KarypisLab 2022] Prof. George Karipis’s research group, “METIS”, https://github.com/KarypisLab/METIS, 2022. • [Baerentzen 2021] Andres Baerentzen, Eva Rotenberg, “Skeletonization via Local Separators”, ACM Transaction on Graphics, Vol.40, Issue 5, No.187, pp.1-18, 2021. • [monsho 2023] もんしょ, “もんしょの巣穴 DirectXの話 第182回 Visibility Buffer”, https://sites.google.com/site/monshonosuana/directx%E3%81%AE%E8%A9%B1/directx%E3%81%AE%E8%A9%B1-%E7%AC%AC182%E5%9B%9E, 2023. • [Ciardi 2018] Francesco Cifariello Ciardi, “Intro to GPU Scalarization – Part 1”, https://flashypixels.wordpress.com/2018/11/10/intro-to-gpu-scalarization-part- 1/ • [Shocker 2023] Shocker_0x15, “現代のGPUの実行スタイルとレイトレ(2023)”, https://speakerdeck.com/shocker_0x15/modern-gpu-execution-and-ray-tracing, レイトレ 合宿9 セミナー, • [Doghramachi 2017] Hawar Doghramachi, Jean-Normand Bucci, “Deferred+: Next-Gen Culling and Rendering for the Dawn Engine”, GPU Zen, pp.77-104, 2017. 273

Slide 270

Slide 270 text

• [Yu 2024] 于 承中, 内村 創, “ニューラルネットワークを用いた高精度なオブジェクトカリングシステムの提案”, CEDEC + KYUSHU 2024. • [Yu 2025] 于 承中, 内村 創, “NueralPVS: 『グランツーリスモ7』におけるニューラルネットワークを用いた次世代オブジェクトカリングシステム”, CEDEC 2025. 274

Slide 271

Slide 271 text

275