RenderPassを使うことでいくつかのポストエフェクトを効率的に実行できるようになります。ここでは0.0f~1.0fでクリッピングを行うだけの単純なトーンマッピングを実装していきます。その過程でシェーダでのSubpassInputの使い方を解説していきます。
前準備
RenderPassについての詳細は前日の記事を参照してください。
仕様策定
今回実装するトーンマッピングは、次に示すようなパスで実装を行っていきます。
図中のRrおよびOutはリソース(Image)で、次のようなパラメータで生成されているものとします。
- Rr: Format=
VK_FORMAT_R16G16B16A16_SFLOAT
- Out: Format=
VK_FORMAT_R8G8B8A8_UNORM
RenderPassの作成
ではまずAttachmentから定義していきます。
const attachmentRr = VkAttachmentDescription { 0, VK_FORMAT_R16G16B16A16_SFLOAT, VK_SAMPLE_COUNT_1_BIT,
VK_ATTACHMENT_LOAD_OP_CLEAR, VK_ATTACHMENT_STORE_OP_DONT_CARE,
VK_ATTACHMENT_LOAD_OP_DONT_CARE, VK_ATTACHMENT_STORE_OP_DONT_CARE,
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
const attachmentOut = VkAttachmentDescription { 0, VK_FORMAT_R8G8B8A8_UNORM, VK_SAMPLE_COUNT_1_BIT,
VK_ATTACHMENT_LOAD_OP_DONT_CARE, VK_ATTACHMENT_STORE_OP_STORE,
VK_ATTACHMENT_LOAD_OP_DONT_CARE, VK_ATTACHMENT_STORE_OP_DONT_CARE,
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL };
通常のレンダリングを受けるほうは内容をクリアする必要がありますが、最終的にデータが必要というわけではないのでその指定を行っています。逆に出力側は全ピクセル出力されるので前のピクセルはどうでもいいのと、後のRenderPassで使用することもあるのでメモリに書き戻すようにしています。また、シェーダ入力(テクスチャ)として使用できるようにfinalLayoutを指定しています。
次にSubpassを定義します。
const passNormalRenderOutput = VkAttachmentReference { 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL });
const passTonemapSource = VkAttachmentReference { 0, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL };
const passTonemapOutput = VkAttachmentReference { 1, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
const passNormalRender = VkSubpassDescription { 0, VK_PIPELINE_BIND_POINT_GRAPHICS,
0, nullptr, 1, &passNormalRenderOutput, nullptr, nullptr, 0, nullptr };
const passTonemap = VkSubpassDescriptor { 0, VK_PIPELINE_BIND_POINT_GRAPHICS,
1, &passTonemapSource, 1, &passTonemapOutput, nullptr, nullptr, 0, nullptr};
最初の3行でAttachmentへの参照を定義しています。出力はCOLOR_ATTACHMENT_OPTIMAL、入カはSHADER_READ_ONLY_OPTIMALである必要があるためその設定をしています。残りの4行でパスの定義をしています。
最後に依存性の定義です。今回はパス1の実行より前にパス0が完了している必要があるためそれを記述します。
const depNormalToTone = VkSubpassDependency { 0, 1, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
VK_ACCESS_SHADER_READ_BIT, VK_DEPENDENCY_BY_REGION_BIT };
全てのピクセルに対して完了している必要はないのでDEPENDENCY_BY_REGION_BITを指定しています。
最後にRenderPassを作成します。
const attachments = std::make_unique<VkAttachmentDescription[2]>({ attachmentRr, attachmentOut });
const passes = std::make_unique<VkSubpassDescription[2]>({ passNormalRender, passTonemap });
const rpinfo = VkRenderPassCreateInfo { VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO, nullptr, 0,
2, attachments.get(), 2, passes.get(), 1, &depNormalToTone };
auto res = vkCreateRenderPass(device, &rpinfo, nullptr, &renderPass);
トーンマップシェーダの作成
今回は単純にクランプのみを行うシェーダを作成します。頂点シェーダは単純に入力頂点を出力するのみのものを使うとして、以下にフラグメントシェーダのコードを示します。
layout(location = 0) out vec4 color;
layout(set = 0, binding = 0, input_attachment_index = 0) uniform subpassInput intex;
void main() { color = clamp(subpassLoad(intex), vec4(0.0f), vec4(1.0f)); }
2行目の定義が入力アタッチメントを使用する際の定義となります。入力アタッチメントは一種のユニフォーム値として受け渡されて、PipelineLayoutにも定義を行う必要があります。入力アタッチメントはテクスチャではないためsubpassInput
という独自の型を使って表します。
subpassInput
からピクセルデータを読み込むにはsubpassLoad
を使用します。subpassLoad
にはsubpassInput
しか渡すことができませんが、これはラスタライズしているピクセルに対応したピクセルのデータしか取得することができなくなっているためです。このためブラーなどといった処理はsubpassInput
を用いた実装はできません。
入力アタッチメントを受け渡す準備
先のセクションで扱ったように、入力アタッチメントは一種のユニフォーム値として受け渡されるためPipelineLayoutに登録する必要があり、ということは対応するDescriptorSetLayoutも作成する必要があります。
入力アタッチメントが対応するVkDescriptorType
はVK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT
なのでそれを指定します。
const descbindToneInput = VkDescriptorSetBinding { 0,
VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, 1,
VK_SHADER_STAGE_FRAGMENT_BIT, nullptr };
ここまでくれば、あとは普通にFramebufferを用意してパイプラインを構成して描画を行うだけです。サンプル全体のコードはありませんがこんなこともできますといった感じで。他には、第二パスで全体を覆うポリゴンではなくて特定の領域のみ覆うポリゴンなどで描画を発行すれば疑似的にステンシルみたいなことができると思います(効率は良くない気がしますが)。