メインコンテンツへスキップ
Tech Playground
ゲーム開発

レンダリングパイプラインで学ぶ遅延シェーディングの実装と最適化手法

遅延シェーディング(Deferred Shading)の仕組みとG-Bufferの最適化を、Vulkan/DirectX12の実装例で解説。タイルベースレンダリングや2026年最新の手法まで。

約9分で読めます

遅延シェーディングとは何か?なぜ必要なのか

現代の3Dゲームエンジンでは、1フレームに数百〜数千のライトを処理する必要があるシーンも珍しくありません。従来のフォワードレンダリングでは、各ピクセルに対してすべてのライトの影響を計算するため、ライト数に比例してパフォーマンスが劣化します。

遅延シェーディング(Deferred Shading) は、この問題を解決するスクリーン空間のシェーディング技術です。レンダリングを2つのパスに分割することで、複雑なライティング計算を効率化します:

  1. ジオメトリパス:ポリゴンの位置・法線・マテリアル情報をG-Buffer(Geometry Buffer)に書き込む
  2. ライティングパス:G-Bufferから取得したデータを使って、スクリーン空間でライティング計算を実行

この手法により、各ピクセルのライティング計算は実際に画面に描画されるピクセル(可視領域)に対してのみ実行されるため、オーバードローによる計算の無駄がなくなります。

レンダリングパイプラインの構成とG-Bufferの設計

G-Bufferに格納すべきデータ

遅延シェーディングの効率性は、G-Bufferの設計に大きく依存します。典型的なG-Bufferは以下の情報を複数のレンダーターゲット(MRT: Multiple Render Targets)に格納します:

// G-Buffer レイアウト例(DirectX12 / Vulkan)
struct GBuffer {
    Texture2D RT0_Albedo;       // RGB: Albedo (基本色), A: Metallic
    Texture2D RT1_Normal;       // RG: 圧縮された法線, B: Roughness, A: AO
    Texture2D RT2_Position;     // RGB: ワールド空間位置, A: 未使用
    Texture2D RT3_Emissive;     // RGB: Emissive, A: マテリアルID
    Texture2D DepthStencil;     // 深度・ステンシルバッファ
};

メモリ最適化のポイント

  • 位置情報の再構築:RT2_Positionを削除し、深度バッファとカメラ逆行列から位置を再計算することで、32bit RGBA分のメモリを節約可能
  • 法線の圧縮:Octahedron Encoding(球面座標を正八面体に投影)で法線を2成分(RG)に圧縮
  • パッキング:1ピクセルあたり16バイト程度に収めることで、4K解像度でも約128MBに抑制

ジオメトリパスの実装例(Vulkan)

// Vulkan: ジオメトリパス用のパイプライン設定
VkPipelineColorBlendAttachmentState colorBlendAttachments[4] = {};
for (int i = 0; i < 4; i++) {
    colorBlendAttachments[i].colorWriteMask = 
        VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
        VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
    colorBlendAttachments[i].blendEnable = VK_FALSE;
}

VkPipelineColorBlendStateCreateInfo colorBlending = {};
colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
colorBlending.attachmentCount = 4; // G-Buffer の MRT 数
colorBlending.pAttachments = colorBlendAttachments;

// フラグメントシェーダー: G-Buffer への書き込み
layout(location = 0) out vec4 outAlbedo;
layout(location = 1) out vec4 outNormal;
layout(location = 2) out vec4 outPosition;
layout(location = 3) out vec4 outEmissive;

void main() {
    outAlbedo = vec4(albedo.rgb, metallic);
    outNormal = vec4(EncodeOctahedron(normalize(normal)), roughness, ao);
    outPosition = vec4(worldPos, 1.0);
    outEmissive = vec4(emissive, materialID);
}

ライティングパスの実装と最適化

フルスクリーンクワッドによるライティング

ライティングパスでは、G-Bufferをテクスチャとしてサンプリングしながら、フルスクリーンの矩形(Quad)を描画します。

// フラグメントシェーダー: ライティングパス
layout(binding = 0) uniform sampler2D gAlbedo;
layout(binding = 1) uniform sampler2D gNormal;
layout(binding = 2) uniform sampler2D gPosition;
layout(binding = 3) uniform sampler2D gDepth;

layout(location = 0) out vec4 FragColor;

void main() {
    vec2 uv = gl_FragCoord.xy / screenSize;
    
    // G-Buffer からデータ取得
    vec3 albedo = texture(gAlbedo, uv).rgb;
    float metallic = texture(gAlbedo, uv).a;
    vec3 normal = DecodeOctahedron(texture(gNormal, uv).rg);
    float roughness = texture(gNormal, uv).b;
    
    // 深度から位置を再構築(メモリ節約版)
    float depth = texture(gDepth, uv).r;
    vec3 worldPos = ReconstructPosition(uv, depth, invViewProj);
    
    // ライティング計算(PBR)
    vec3 finalColor = vec3(0.0);
    for (int i = 0; i < lightCount; i++) {
        finalColor += CalculatePBR(albedo, normal, roughness, metallic, 
                                   worldPos, lights[i]);
    }
    
    FragColor = vec4(finalColor, 1.0);
}

タイルベース遅延レンダリング(2026年のスタンダード)

単純な遅延シェーディングでは、すべてのライトをすべてのピクセルで評価するため、ライト数が増えると非効率です。タイルベース遅延レンダリング(Tiled Deferred Rendering) は、この問題を解決する2026年の業界標準手法です。

仕組み

  1. スクリーンを16×16ピクセルのタイルに分割
  2. 各タイルに影響を与えるライトのリストを事前にカリング(Compute Shader使用)
  3. ライティングパスでは、各タイルが関連するライトのみを処理
// Compute Shader: タイルごとのライトカリング
#version 450
layout(local_size_x = 16, local_size_y = 16) in;

shared uint tileMinDepthInt;
shared uint tileMaxDepthInt;
shared uint tileLightIndices[MAX_LIGHTS_PER_TILE];
shared uint tileLightCount;

void main() {
    ivec2 tileID = ivec2(gl_WorkGroupID.xy);
    ivec2 pixelPos = ivec2(gl_GlobalInvocationID.xy);
    
    // タイル内の深度範囲を計算
    float depth = texelFetch(gDepth, pixelPos, 0).r;
    atomicMin(tileMinDepthInt, floatBitsToUint(depth));
    atomicMax(tileMaxDepthInt, floatBitsToUint(depth));
    
    barrier();
    
    // タイルのフラスタムを計算
    Frustum tileFrustum = CreateTileFrustum(tileID, tileMinDepthInt, tileMaxDepthInt);
    
    // ライトのカリング(各スレッドが一部を担当)
    for (uint i = gl_LocalInvocationIndex; i < totalLightCount; i += 256) {
        if (SphereIntersectsFrustum(lights[i], tileFrustum)) {
            uint index = atomicAdd(tileLightCount, 1);
            tileLightIndices[index] = i;
        }
    }
    
    barrier();
    
    // タイルのライトリストをグローバルバッファに書き込み
    if (gl_LocalInvocationIndex == 0) {
        uint offset = tileID.y * numTilesX + tileID.x;
        tileLightLists[offset].count = tileLightCount;
        for (uint i = 0; i < tileLightCount; i++) {
            tileLightLists[offset].indices[i] = tileLightIndices[i];
        }
    }
}

この手法により、数千のライトでもリアルタイム処理が可能になります。モバイルGPU(Adreno 610+、Mali G70+)でも、タイルベースアーキテクチャとの親和性が高く効率的です。

遅延シェーディングの課題と対策

透明オブジェクトの扱い

遅延シェーディングはスクリーン空間のピクセルごとに1つの情報しか保持できないため、半透明オブジェクトには使用できません

解決策

  • 不透明オブジェクト:遅延シェーディング
  • 半透明オブジェクト:フォワードレンダリングで別パスとして描画
// ハイブリッドアプローチ(DirectX12)
commandList->SetPipelineState(deferredPSO);
RenderOpaqueObjects();  // 遅延シェーディング

commandList->SetPipelineState(forwardPSO);
RenderTransparentObjects();  // フォワードレンダリング

アンチエイリアシング(MSAA)の非互換性

G-BufferではMSAA(Multi-Sample Anti-Aliasing)が使えないため、代替手法が必要です。

推奨手法

  • TAA(Temporal Anti-Aliasing):過去のフレーム情報を活用
  • FXAA / SMAA:ポストプロセスで実装可能
  • DLSS / FSR:AIベースのアップスケーリング(2026年では標準装備)

メモリ消費量の削減

4K解像度(3840×2160)で4つのRTを使用する場合、約132MBのVRAMが必要です。

最適化手法

  1. Deferred Lighting(Light Pre-Pass):G-Bufferにマテリアル情報を格納せず、ライティング結果のみを保存
  2. 圧縮フォーマット:BC圧縮を使用してメモリを1/4〜1/8に削減(品質とのトレードオフ)
  3. 動的解像度スケーリング:負荷に応じてG-Bufferの解像度を調整

まとめ:2026年の遅延シェーディング実装のベストプラクティス

  • タイルベースレンダリングが標準:16×16タイルでライトカリングを行い、数千のライトをリアルタイム処理
  • Compute Shaderの活用:DirectX12/Vulkanでは、カリング処理をGPU並列化することでパフォーマンス大幅向上
  • G-Bufferの最適化:位置情報の再構築、法線圧縮、適切なパッキングで16バイト/ピクセル以下を目指す
  • ハイブリッドアプローチ:不透明オブジェクトは遅延、半透明はフォワードで描画
  • モバイル対応:Adreno 610+、Mali G70+などのタイルベースGPUでは、遅延レンダリングが効率的
  • ポストプロセスAA:MSAA非対応のため、TAA/DLSS/FSRを標準装備

遅延シェーディングは複雑なライティングを持つ現代のゲームには不可欠な技術です。Vulkan/DirectX12の低レベルAPIを活用することで、さらなる最適化の余地があります。

Sources:

#レンダリングパイプライン #遅延シェーディング #Vulkan #DirectX12 #G-Buffer
シェア: