Vulkan - 8. 여러 서브패스에 걸친 렌더 패스

2022. 6. 26. 15:44Vulkan

 

개요

여러 서브패스에 걸친 렌더 패스는 여러 개의 결과를 합칠 경우에 쓸만합니다. 아직은 수학 라이브러리를 제대로 갖춰 놓은 상황도 아니고 하니 대단한 건 일단 됐고, 기존에 그리던 것에다가 간단하게 반전(negative) 필터링해보는 시간을 가져 봅시다.

 

이 코드는 여기를 참고하였습니다. 당장 디퍼드 렌더링이 필요하다면 저길 참고하시는 게 좋겠죠.

 

목차

1. 렌더패스에서 여러 서브 패스를 쓰게 구성하기

2. 프레임버퍼 재구성

3. 후처리용 파이프라인 만들기

4. 또 또 기술자 집합

5. 2번째 서브패스를 사용하도록 명령 작성하기

요약

과제

 

본문

1. 렌더패스에서 여러 서브 패스를 쓰게 구성하기

단순히 메시를 프레임버퍼에 그리는 패스는 이미 준비돼 있어. 그에 대해 필터링을 적용하려면 뭐가 더 필요할까?
이전 프레임버퍼의 색 첨부물을 텍스처로 쓰게 하는, 새 파이프라인이 필요하겠네. 지금 각 스왑체인 이미지 뷰를 참조하는 프레임버퍼는 두고 중간 프레임버퍼를 새로 만들어서 새 색 첨부물이랑 깊이 버퍼를 써야겠지?

 

색 첨부물과 같은 이미지를 텍스처로 하기 위한 지식 자체는 이미 갖고 있습니다(같은 렌더패스 아래에서는 사용할 수 없습니다. 글의 맨 아래 부분을 참고하세요). 하지만 이번에는 다른 걸 알아봅시다. 벌칸에서 여러 서브패스 간의 소통 방법 중 하나는 입력용 첨부물입니다. 입력용 첨부물은 그걸 이용하는 조각 셰이더에서, 각 조각 위치에 일대일로 대응하여 값을 가져옵니다. 다음 서브패스로 넘어간다면 이 입력용 첨부물의 작성이 완료될 때까지 기다려야겠죠? 코드를 작성해 봅시다.

 

더보기

일단 첫 번째 서브패스 자체에서 고칠 건 없습니다. 바인드할 프레임버퍼와 첨부물만 새로 만들면 됩니다. 두 번째 서브패스는 앞 서브패스에서 색 첨부물을 그대로 받아 이용하고, 새로 작성할 색 첨부물을 만들면 되겠습니다.

//VkPlayer::createRenderPass0
VkAttachmentDescription attachments[3]{};
VkAttachmentDescription& colorAttachment = attachments[0];
VkAttachmentDescription& depthAttachment = attachments[1];
VkAttachmentDescription& middleColorAttachment = attachments[2];
...
middleColorAttachment.format = swapchainImageFormat;
middleColorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
middleColorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
middleColorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
middleColorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
middleColorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
middleColorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
middleColorAttachment.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

이제 첫 번째 서브패스는 이 2번 첨부물을 색 첨부물로 쓰게 해야 하겠죠. 두 번째 서브패스는 그걸 받아오고요.

VkAttachmentReference colorAttachmentRef{};
colorAttachmentRef.attachment = 2;
colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
...
VkAttachmentReference inputAttachmentRef{};
inputAttachmentRef.attachment = 2;
inputAttachmentRef.layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;

VkAttachmentReference finalColor{};
finalColor.attachment = 0;
finalColor.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

그 다음 2번째 서브패스를 만들어 줍니다.

VkSubpassDescription subpasses[2] = {};
VkSubpassDescription& subpass0 = subpasses[0];
VkSubpassDescription& subpass1 = subpasses[1];

subpass0.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass0.colorAttachmentCount = 1;
subpass0.pColorAttachments = &colorAttachmentRef;
subpass0.pDepthStencilAttachment = &depthAttachmentRef;

subpass1.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass1.colorAttachmentCount = 1;
subpass1.pColorAttachments = &finalColor;
subpass1.inputAttachmentCount = 1;
subpass1.pInputAttachments = &inputAttachmentRef;

그 다음 중간 렌더링과 최종 렌더링 사이의 의존성 조건을 추가합니다. '더보기' 직전에 얘기한 대로만 하면 돼요.

VkSubpassDependency dependencies[2] = {};
VkSubpassDependency& dependency0 = dependencies[0];
VkSubpassDependency& dependency1 = dependencies[1];
...
dependency1.srcSubpass = 0;
dependency1.dstSubpass = 1;
dependency1.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency1.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
dependency1.dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
dependency1.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
dependency1.dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT;

기존의 의존성은 여길 수정할 필요가 있습니다.

dependency0.dstSubpass = 1;

 

 

여기까지 제대로 만들었다면 확인계층이 대충 프레임버퍼를 만들려고 했더니 렌더패스랑 안 맞는다고 불평합니다.

 

2. 프레임버퍼 재구성

프레임버퍼를 렌더패스에서 쓸 수 있게 하기 위해 해야 할 일은 이렇게 됩니다.

  • 스왑체인 뷰를 참조하고 있는 최종 프레임버퍼(들)이 이전 패스의 색 버퍼(A)와 새로 작성할 색 버퍼의 이미지 뷰(B)를 참조하게 합니다. 깊이/스텐실(C)은 이제 여기선 필요 없습니다.
  • 첫 번째 패스에서 새 색 버퍼(A)와 깊이/스텐실(C)를 참조할 프레임버퍼를 만들어 줍니다.
위 리스트에서 기호 A, B, C를 썼잖아. 저 중에서 기존에 쓰던 스왑체인 이미지 뷰는 뭘까?
B!

서브패스가 참조하는 첨부물 인덱스는 프레임버퍼의 첨부물 배열 인덱스이니, 당연히 프레임버퍼에 각각 첨부물을 3개씩 넣어야 합니다. 깊이/스텐실 때랑 마찬가지로 중간 이미지는 하나만 쓰면 됩니다. 코드를 작성합시다.

 

더보기

다시 이미지 뷰를 만들 때가 왔습니다. 스왑체인 이미지와 사양을 맞춰야 하니 스왑체인 생성 후, 프레임버퍼 생성 전에 하면 되겠죠? 지금은 편의상 별도의 함수를 만들지는 않고 깊이/스텐실 버퍼 함수에서 같이 만들겠습니다. 이 이미지 역시, CPU에서 직접적으로 넘길 데이터가 없으니 딱히 스테이징은 필요 없습니다.

 

새 멤버를 만들어 줍시다.

// VkPlayer.h
static VkImage middleImage;
static VkDeviceMemory middleMem;
static VkImageView middleImageView;

해제 함수에 이것도 해제하게 추가해요.

void VkPlayer::destroyDSBuffer() {
    vkDestroyImageView(device, middleImageView, nullptr);
    vkDestroyImageView(device, dsImageView, nullptr);
    vkFreeMemory(device, middleMem, nullptr);
    vkFreeMemory(device, dsmem, nullptr);
    vkDestroyImage(device, middleImage, nullptr);
    vkDestroyImage(device, dsImage, nullptr);
}

이젠 익숙할 겁니다. 이미지부터 뷰 생성까지는 빠르게 넘어갑시다.

// VkPlayer::createDSBuffer
... (이전 return true 대신)
imgInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT;
imgInfo.format = swapchainImageFormat;
if (vkCreateImage(device, &imgInfo, nullptr, &middleImage) != VK_SUCCESS) {
    fprintf(stderr, "Failed to create intermediate image\n");
    return false;
}
vkGetImageMemoryRequirements(device, middleImage, &mreq);
memInfo.allocationSize = mreq.size;
memInfo.memoryTypeIndex = findMemorytype(mreq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, physicalDevice.card);
if (vkAllocateMemory(device, &memInfo, nullptr, &middleMem) != VK_SUCCESS) {
    fprintf(stderr, "Failed to allocate intermediate image memory\n");
    return false;
}
if (vkBindImageMemory(device, middleImage, middleMem, 0) != VK_SUCCESS) {
    fprintf(stderr, "Failed to bind intermediate image - memory\n");
    return false;
}
viewInfo.image = middleImage;
viewInfo.format = imgInfo.format;
viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
if (vkCreateImageView(device, &viewInfo, nullptr, &dsImageView) != VK_SUCCESS) {
    fprintf(stderr, "Failed to create intermediate image view\n");
    return false;
}

return true;

이제 프레임버퍼 생성 함수로 가서 렌더패스와 첨부물 아다리를 맞춥니다.

// VkPlayer::createEndFramebuffers
...
VkImageView attachments[] = { nullptr,dsImageView,middleImageView };
for (size_t i = 0; i < swapchainImageViews.size(); i++) {
    attachments[0] = swapchainImageViews[i];
    info.attachmentCount = sizeof(attachments) / sizeof(attachments[0]);
    info.pAttachments = attachments;
    if (vkCreateFramebuffer(device, &info, nullptr, &endFramebuffers[i]) != VK_SUCCESS) {
        fprintf(stderr, "Failed to create framebuffer\n");
        return false;
    }
}
return true;

쉽죠?

 

여기까지 왔다면 당연히 실제 창과 연결된 2번째 서브패스를 안 쓰고 있기 때문에 화면엔 아무 것도 안 그려지고 확인 계층이 켜져 있다면 많은 수의 오류가 나올 겁니다. 프레임버퍼 첨부물은 이제 다 했는데 눈에 당장 보이는 오류 수라도 줄이면 좋을 것 같습니다. 렌더패스 시작 정보 구조체의 클리어 값을 잘 맞춰서 주도록 합시다. 아래 부분만 수정하면 오류가 한 종류로 줄 겁니다.

// VkPlayer::fixedDraw
...
VkClearValue clearValue[] = { clearColor, clearDepthStencil, clearColor };
...

 

 

3. 후처리용 파이프라인 만들기

후처리용 파이프라인은 별로 할 일이 없습니다. 입력 첨부물에서 값을 읽은 뒤 거기다가 뭔가 하는 게 끝이죠. 겁 먹을 거 없이 바로 만들어 봅시다.

 

더보기

여느 때처럼 멤버를 만듭니다.

// VkPlayer.h
static VkPipeline pipeline1;

static bool createPipeline1();
static void destroyPipeline1();

일단 셰이더부터 작성해 볼까요?  지금 쓸 건 텍스처가 아니기 때문에 GL 때 알던 거랑 약간 다릅니다.

#version 450
// #extension GL_KHR_vulkan_glsl: enable

const vec2 pos[3] = {vec2(-1, -1), vec2(-1, 3), vec2(3, -1)};
const vec2 tc[3] = {vec2(0, 0), vec2(0, 2), vec2(2, 0)};

void main() {
    gl_Position = vec4(os[gl_VertexIndex], 0.0f, 1.0f);
}

여기선 고정된 삼각형을 그립니다. 척 보시면 알겠지만 뷰포트 전체에 걸친 직사각형(삼각형의 일부)에 대하여 텍스처좌표를 쓰고 싶다면 pos[gl_VertexIndex]에 1을 더하고 0.5를 곱하여 넘기면 됩니다.

// post.frag
#version 450

layout(location = 0) out vec4 outColor;

layout(input_attachment_index = 0, binding = 0) uniform subpassInput inColor;

void main() {
    outColor = vec4(1.0) - subpassLoad(inColor);
    outColor.a = 1.0;
}

이제 파이프라인 정보를 입력하여 생성합니다. 이 파이프라인은 더 간단히 생성할 수 있는데, 먼저 아까 그 셰이더 모듈을 컴파일해서 올립니다.

bool VkPlayer::createPipeline1() {
    VkShaderModule vertModule = createShaderModule("post.vert", shaderc_shader_kind::shaderc_glsl_vertex_shader);
    VkShaderModule fragModule = createShaderModule("post.frag", shaderc_shader_kind::shaderc_glsl_fragment_shader);
    VkPipelineShaderStageCreateInfo shaderStagesInfo[2] = {};
    shaderStagesInfo[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
    shaderStagesInfo[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
    shaderStagesInfo[0].module = vertModule;
    shaderStagesInfo[0].pName = "main";

    shaderStagesInfo[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
    shaderStagesInfo[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
    shaderStagesInfo[1].module = fragModule;
    shaderStagesInfo[1].pName = "main";

정점 속성은 구성할 필요가 없고 입력 어셈블리는 기존이랑 같습니다.

VkPipelineVertexInputStateCreateInfo vertexInputInfo{};
vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;

VkPipelineInputAssemblyStateCreateInfo inputAssemblyInfo{};
inputAssemblyInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
inputAssemblyInfo.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;	// 3개씩 끊어 삼각형
inputAssemblyInfo.primitiveRestartEnable = VK_FALSE;	// 0xffff 혹은 0xffffffff 인덱스로 스트립 끊기 가능 여부

뷰포트, 시저도 기존과 같습니다.

VkViewport viewport{};
viewport.x = 0.0f;
viewport.y = 0.0f;
viewport.width = (float)swapchainExtent.width;
viewport.height = (float)swapchainExtent.height;
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;

VkRect2D scissor{};
scissor.offset = { 0,0 };
scissor.extent = swapchainExtent;

VkPipelineViewportStateCreateInfo viewportStateInfo{};
viewportStateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportStateInfo.viewportCount = 1;
viewportStateInfo.pViewports = &viewport;
viewportStateInfo.scissorCount = 1;
viewportStateInfo.pScissors = &scissor;

래스터화 단계도 똑같습니다.

VkPipelineRasterizationStateCreateInfo rasterizerInfo{};
rasterizerInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
rasterizerInfo.depthClampEnable = VK_FALSE;
rasterizerInfo.rasterizerDiscardEnable = VK_FALSE;
rasterizerInfo.polygonMode = VK_POLYGON_MODE_FILL;
rasterizerInfo.lineWidth = 1.0f;
rasterizerInfo.cullMode = VK_CULL_MODE_BACK_BIT;
rasterizerInfo.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
rasterizerInfo.depthBiasEnable = VK_FALSE;
rasterizerInfo.depthBiasConstantFactor = 0.0f;
rasterizerInfo.depthBiasClamp = 0.0f;
rasterizerInfo.depthBiasSlopeFactor = 0.0f;

멀티샘플링으로 효과를 보려면 1차 패스에서 미리 썼어야 하고 여기선 써도 의미 없습니다.

VkPipelineMultisampleStateCreateInfo msInfo{};
msInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
msInfo.sampleShadingEnable = VK_FALSE;
msInfo.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
msInfo.minSampleShading = 1.0f;
msInfo.alphaToCoverageEnable = VK_FALSE;
msInfo.alphaToOneEnable = VK_FALSE;
msInfo.pSampleMask = nullptr;

깊이와 스텐실버퍼 안 씁니다.

VkPipelineDepthStencilStateCreateInfo depthStencilInfo{};
depthStencilInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;

블렌딩 할 일은 없는데 일단 그대로 씁니다.

VkPipelineColorBlendAttachmentState colorBlendAttachmentState{};
colorBlendAttachmentState.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
colorBlendAttachmentState.blendEnable = VK_TRUE;
colorBlendAttachmentState.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
colorBlendAttachmentState.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
colorBlendAttachmentState.srcAlphaBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
colorBlendAttachmentState.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
colorBlendAttachmentState.colorBlendOp = VK_BLEND_OP_ADD;
colorBlendAttachmentState.alphaBlendOp = VK_BLEND_OP_ADD;

셰이더에 uniform을 보시면 알겠지만 기술자 집합은 하나 필요한데, 파이프라인 레이아웃은 일단 보류합시다.

VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipelineLayout1);

파이프라인을 생성합니다. 여기선 렌더패스의 1번(두 번째) 서브패스에 호환되어야 함을 기억하세요.

VkGraphicsPipelineCreateInfo pipelineInfo{};
pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
pipelineInfo.stageCount = sizeof(shaderStagesInfo) / sizeof(shaderStagesInfo[0]);
pipelineInfo.pStages = shaderStagesInfo;
pipelineInfo.pVertexInputState = &vertexInputInfo;
pipelineInfo.pInputAssemblyState = &inputAssemblyInfo;
pipelineInfo.pViewportState = &viewportStateInfo;
pipelineInfo.pRasterizationState = &rasterizerInfo;
pipelineInfo.pMultisampleState = &msInfo;
pipelineInfo.pDepthStencilState = &depthStencilInfo;
pipelineInfo.pColorBlendState = &colorBlendInfo;
pipelineInfo.pDynamicState = nullptr;
pipelineInfo.layout = pipelineLayout1; // 여기랑
pipelineInfo.renderPass = renderPass0;
pipelineInfo.subpass = 1; // 여기

pipelineInfo.basePipelineHandle = VK_NULL_HANDLE;
pipelineInfo.basePipelineIndex = -1;
vkDestroyShaderModule(device, vertModule, nullptr);
vkDestroyShaderModule(device, fragModule, nullptr);

VkResult result = vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline1);
if (result != VK_SUCCESS) {
    fprintf(stderr, "Failed to create pipeline 1\n");
    return false;
}
return true;

 

 

아마 pipelineLayout1이 아직 안 돼서 그 관련 오류가 뜰 겁니다. 이건 이따가 서브패스 쓸 때 바인드할 겁니다.

 

4. 또 또 기술자 집합

uniform이란 이름을 봤으면 왜 기술자 집합이 나오는지 알겠죠. 그래도 얘는 작성 자체를 GPU에서 하기 때문에, 그보다 그 이전에 객체별 데이터가 아니니 동기화로 골머리 안 썩어도 됩니다. (이는 텍스처 객체를 만들어 써도 마찬가지입니다.) 이미지(뷰) 생성 후, 파이프라인 생성 전에 이 기술자들을 만들어 둡시다.

 

더보기

createDescriptorSet에서 2번째 서브패스를 위한 기술자 풀을 만들어 할당해 봅시다. 멤버를 만들어 보죠.

// VkPlayer.h
static VkDescriptorSetLayout sp1layout;
static VkDescriptorPool sp1pool;
static VkDescriptorSet sp1set;

해당 함수의 끝부분에 이만큼 추가해 줍시다.

VkDescriptorSetLayoutBinding sp1binding{};
sp1binding.binding = 0;
sp1binding.descriptorType = VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT;
sp1binding.descriptorCount = 1;
sp1binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
info.bindingCount = 1;
info.pBindings = &sp1binding;
if (vkCreateDescriptorSetLayout(device, &info, nullptr, &sp1layout) != VK_SUCCESS) {
    fprintf(stderr, "Failed to create descriptor set layout for input attachment\n");
    return false;
}

VkDescriptorPoolSize sp1poolSize{};
sp1poolSize.descriptorCount = 1;
sp1poolSize.type = VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT;

dpinfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
dpinfo.poolSizeCount = 1;
dpinfo.pPoolSizes = &sp1poolSize;
dpinfo.maxSets = 1;

if (vkCreateDescriptorPool(device, &dpinfo, nullptr, &sp1pool) != VK_SUCCESS) {
    fprintf(stderr, "Failed to create descriptor pool for input attachment\n");
    return false;
}
setInfo.descriptorPool = sp1pool;
setInfo.descriptorSetCount = 1;
setInfo.pSetLayouts = &sp1layout;
if (vkAllocateDescriptorSets(device, &setInfo, &sp1set) != VK_SUCCESS) {
    fprintf(stderr, "Failed to allocate descriptor set for input attachment\n");
    return false;
}

VkDescriptorImageInfo imageInfo{};
imageInfo.imageView = middleImageView;
imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkWriteDescriptorSet descriptorWrite{};
descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrite.dstSet = sp1set;
descriptorWrite.dstBinding = 0;
descriptorWrite.dstArrayElement = 0;
descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT;
descriptorWrite.descriptorCount = 1;
descriptorWrite.pImageInfo = &imageInfo;
vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr);

했던 데서 그냥 옵션만 살짝 바꿨습니다. 천천히 읽어 보면서 이전에 했던 내용들을 떠올려 보세요.

 

이제 방금 파이프라인 레이아웃 생성에 이 정보를 넣어 줍시다.

 

// VkPlayer::createPipeline1
...
VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 1;
pipelineLayoutInfo.pSetLayouts = &sp1layout;
vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipelineLayout1);
...

 

아직 뵈는 게 없어 괴롭겠지만 다 왔습니다. 어차피 지금까지 한 건 셰이더 빼면 이전 것의 반복이었어요.

 

5. 2번째 서브패스를 사용하도록 명령 작성하기

이젠 다 만들어진 걸 쓰기만 하면 그만입니다. 그리고 나서 결과를 확인해 보죠. fixedDraw 함수에서 vkCmdEndRenderpass 함수 직전에 이것들을 넣으면 끝입니다. 핵심은 vkCmdNextSubpass인데, begin info는 필요 없고 단순히 다음 서브패스를 시작하는 겁니다.

 

// VkPlayer::fixedDraw
...
vkCmdNextSubpass(commandBuffers[commandBufferNumber], VK_SUBPASS_CONTENTS_INLINE);
vkCmdBindPipeline(commandBuffers[commandBufferNumber], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline1);
vkCmdBindDescriptorSets(commandBuffers[commandBufferNumber], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout1, 0, 1, &sp1set, 0, nullptr);
vkCmdDraw(commandBuffers[commandBufferNumber], 3, 1, 0, 0);
vkCmdEndRenderPass(commandBuffers[commandBufferNumber]);
...

 

결과가 오류 없이 이렇게 나오면 됩니다. 전체 코드는 여기서 확인할 수 있습니다.

 

 

요약

의외로 이번에는 완전히 새로운 내용이라고 할 만한 게 별로 소개되지 않았습니다. 2번 글에서 잘 이해하고 오셨다면 꽤 쉬웠을 겁니다.

 

서브패스에서 이전 것의 입력을 받기 위해 프레임버퍼에 색 첨부물 하나인 이미지 뷰를 추가했고 서브패스의 참조 첨부물 인덱스를 그에 맞췄습니다. 의존성은 이전 패스의 색 출력 이후에 다음 패스의 조각 셰이더를 가동하게 맞추면 되는 거였고, 입력 첨부물은 셰이더에서 input_attachment_index와 디스크립터 바인딩을 맞추고 subpassLoad 함수로 현 픽셀의 대응 값을 가져올 수 있었습니다. 

 

텍스처와 다르게 가져오는 값은 현 픽셀의 위치로 고정되어 구현할 만한 기능에는 한계가 있지만, deffered rendering 등에서는 효과적으로 사용할 수 있습니다.

 

과제

기본적으로 텍스처 출력 후 거기서 샘플링을 원하는 경우, 한 렌더 패스 내의 첨부물 이미지는 같은 렌더 패스 안에서 조합된 샘플러 디스크립터로 사용이 불가능합니다(참고, 스펙의 어디서 그렇다고 하는지는 찾는 중입니다). 때문에 렌더패스를 종료하고 텍스처만 빼서 다른 렌더 패스를 시작하여 이용해야 합니다. 그 사례 중 하나로 이게(그림자맵 구현) 있으니 한번 코드를 읽어 보시면 좋겠네요. 조금 어렵다면 아마 이 코드(텍스처에 그리고서 끝)가 더 간단하지 싶습니다.

 

*별도의 렌더 패스를 쓰는 건 오히려 GL 때랑 사용 방식이 더 비슷하기 때문에 다룰 예정이 없습니다. 다만 동기화와 이미지 레이아웃(dependency와 barrier로 조절)에 주의할 필요가 있으니 실사용 케이스를 참고하는 게 좋습니다.