본문으로 바로가기

Vulkan) 5. 고정 파이프라인 단계

category Graphics/Vulkan 2021. 8. 9. 00:40

모든 내용은 https://vulkan-tutorial.com/Development_environment 에서 발췌합니다.

Windows를 기본으로 합니다. 그 외 플랫폼의 개발 환경 구성은 튜토리얼 사이트를 확인해 주세요.

 

모든 완성된 코드는 github.com/kimduuukbae/VulkanTutorial에 업로드됩니다.


이전에 디바이스 초기화 까지 끝냈습니다.

다음은 고정 파이프라인 단계를 초기화해야 합니다.

 

앞으로 다음과 같은 정보들을 작성할 것 입니다.

 

1. Input Assembly

이 단계는 정점들의 정보를 포함하는 vertex buffer와 index buffer를 읽고 다른 파이프라인 단계에서 사용할 프리미티브로 조립시킨다. ( 이 프리미티브는 점, 라인, 선, 삼각형, 스트립 등 다양 )

또한 System-generated-Value를 추가하는데 이는 시맨틱(Semantic)이라고 하는 문자열이며 다음 단계의 셰이더에서 쓰인다.

 

Input Assembly는 VkPipelineInputAssemblyStateCreateInfo를 사용하여 서술한다.

typedef struct VkPipelineInputAssemblyStateCreateInfo {
    VkStructureType sType;
    const void* pNext;
    VkPipelineInputAssemblyStateCreateFlags flags;
    VklPrimitiveTopology topology;
    VkBool32 primitiveRestartEnable;
} VkPipelineInputAssemblyStateCreateInfo;

sType과 pNext는 이전에 많이 설명했으므로 설명은 생략한다.

sTypeVK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO로 작성된다.

flags는 추후 사용을 위해 예약된 변수이다.

topology는 각 vertex가 어떻게 구성이 되는지를 설명하는 변수이다. topology 목록은 다음과 같다.

  • VK_PRIMITIVE_TOPOLOGY_POINT_LIST : 각 vertex가 점을 생성하는데 사용된다.
  • VK_PRIMITIVE_TOPOLOGY_LINE_LIST : 각 vertex가 쌍으로 묶이며, 첫 vertex에서 두 번째 vertex로의 선분을 형성한다.
  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST : 각 vertex가 3개씩 묶이며, 삼각형을 형성한다.

stripfan도 있다. 이는 vertex를 위에서 설명한 topology에서, 하나 또는 두 개의 vertex를 기존 것과 공유하는 것이다.

  • VK_PRIMITIVE_TOPOLOGY_LINE_STRIP : 첫 두 vertex는 선분을 형성한다. 그 이후의 새로운 vertex들은 마지막으로 처리된 vertex와 새로운 선분을 형성한다. 즉 연결된 선의 연속이다.
  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP : 첫 세 vertex는 삼각형을 형성한다. 그 이후의 각 vertex들은 앞의 삼각형을 형성했던 vertex 중 마지막 두 vertex를 포함하여 새로운 삼각형을 형성한다.
    즉, 연결된 삼각형의 행으로 각 각이 이전과 모서리를 고유한다.
  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN :  첫 세 vertex는 삼각형을 형성한다. 그 이후의 각 vertex들은 앞의 삼각형을 형성했던 vertex중 맨 앞 vertex와 맨 마지막 vertex를 포함하여 삼각형을 형성한다.

 

strip은 (v0, v1, v2)로 첫 삼각형을 만든 다음, (v1, v2, v3)를 이용하여 두 번째 삼각형을 그려낸다.

fan은 (v0, v1, v2)로 첫 삼각형을 만든 다음, (v0, v2, v3)를 이용하여 두 번째 삼각형을 그려낸다. ( 이전 삼각형의 첫, 마지막 vertex만을 사용 )

 

adjacency도 있다. 이는 기하셰이더가 활성화 되고, 원래 mesh에서 다음에 위치한 mesh들의 추가적인 정보를 운반할 수 있을 때만 사용한다.

이는 추후에 기하셰이더를 사용할 때 다시 작성하겠다.

 

primitiveRestartEnable은 strip과 fan을 특별한 인덱스를 통해 분리시킬 수 있습니다. 이는 추후에 다시 설명합니다.

 

2. Viewport

viewport state는 vulkan 파이프라인에서 래스터라이저 이전에 최종 좌표 변환이다.

어떠한 vertex가 월드, 카메라, 투영 행렬을 이용한 변환을 거쳐 투영된 좌표

(정규화된, 라이브러리에 따라 다르지만 vulkan은 (-1. -1) ~ (1, 1))

를 갖게 되는데, 이 투영된 좌표(vertex)를 윈도우 좌표로 다시 바꿔줘야 한다.

윈도우 좌표는 '픽셀' 로 나타내기 때문에 투영된(정규화된) 좌표를 픽셀 좌표로 변경해줘야 할 필요가 있는데,

이를 '뷰포트 변환' 이라고 하며, 뷰포트 변환을 위한 viewport state와 크기를 정의해야 한다.

뷰포트의 크기는 VkViewport로 나타낸다.

typedef struct VkViewport {
    float x;
    float y;
    float width;
    float height;
    float minDepth;
    float maxDepth;
}; VkViewport;

minDepthmaxDepth는 좌표의 z 구성요소를 선형으로 변환시키는데 사용된다.

 

scissor (가위 사각형) 또한 사용되는데, 뷰포트와 비슷하다.

픽셀에 그려질 부분을 선택하는 범위인데, 이 범위에 해당하지 않은 투영된 좌표들은 버려지게 된다. (백버퍼에 래스터화 되지 않음) 예를 들어, UI에 의해 가려지는 부분이 있다면 가위 사각형을 통해 그 부분을 버릴 수 있게 된다.

scissor는 VkRect2D라는, vulkan에서 2D의 사각형을 정의하는 구조체 이며 다른곳에서도 자주 사용된다.

typedef struct VkRect2D{
    VkOffset2D offset;
    VkExtent2D extent;
};

 

예를 들어, 800 x 600 윈도우에, 삼각형을 띄웠다고 하자.

그리고, extent의 height 값을 300으로 나타낸다고 하면.

다음과 같이 절반이 그려지지 않게 된다.

(Fragment shader로도 넘어가지 않고, 래스터라이저가 다 버려버린다.)

 

이제 실제로 viewport state를 정의하여야한다.

typedef struct VkPipelineViewportStateCreateInfo {
    VkStructureType sType;
    const void* pNext;
    VkPipelineViewportStateCreateFlags flags;
    uint32_t viewportCount;
    const VkViewport* pViewports;
    uint32_t scissorCount;
    count VkRect2D* pScissors;
} VkPipelineViewportStateCreateInfo;

VkPipelineViewportSTateCreateInfo 구조체를 작성하는데, 정의는 다음과 같다.

sTypeVK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO로 설정한다.

flags는 불칸을 위해 추후에 사용될지도 몰라, 예약된 플래그 이다.

viewportCount는 viewport의 갯수 이다.

scissorCount는 scissor의 갯수이다. 

 

다음은 본인의 코드에서 viewport state를 정의하는 코드이다.

configInfo.viewport.x = 0.0f;
configInfo.viewport.y = 0.0f;
configInfo.viewport.width = static_cast<float>(width);
configInfo.viewport.height = static_cast<float>(height);
configInfo.viewport.minDepth = 0.0f;
configInfo.viewport.maxDepth = 1.0f;

configInfo.scissor.offset = VkOffset2D{ 0, 0 };
configInfo.scissor.extent = VkExtent2D{ width, height };

configInfo.viewportInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
configInfo.viewportInfo.viewportCount = 1;
configInfo.viewportInfo.pViewports = &configInfo.viewport;
configInfo.viewportInfo.scissorCount = 1;
configInfo.viewportInfo.pScissors = &configInfo.scissor;

 

3. Rasterization

Rasterization은 vertex로 표현되는 primitive가 fragment에서 color를 나타낼 수 있는 픽셀로 변환되는 기본이 되는 처리 과정이다.

Rasterization state에선 

backface culling (은면 제거)

clipping (클리핑)

뷰포트 변환 & scissor 테스트

depth bias 계산 (그림자)

primitive를 그려야할 프레임(렌더 타겟)의 텍셀(픽셀)단위로 변경

을 처리하며, 작성한 순서대로 처리된다.

 

VkPipelineRasterizationStateCreateInfo를 사용하고, 정의는 다음과 같다.

typedef struct VkPipelineRasterizationStateCreateInfo {
    VkStructureType                            sType;
    const void*                                pNext;
    VkPipelineRasterizationStateCreateFlags    flags;
    VkBool32                                   depthClampEnable;
    VkBool32                                   rasterizerDiscardEnable;
    VkPolygonMode                              polygonMode;
    VkCullModeFlags                            cullMode;
    VkFrontFace                                frontFace;
    VkBool32                                   depthBiasEnable;
    float                                      depthBiasConstantFactor;
    float                                      depthBiasClamp;
    float                                      depthBiasSlopeFactor;
    float                                      lineWidth;
} VkPipelineRasterizationStateCreateInfo;

sTypeVK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO로 설정돼야하며

pNext는 nullptr이다.

flags 항목은 예약되었으며 0으로 설정되어야 한다.

depthClampEnable 항목은 depth clamping을 끄고 키는데 사용된다. 만약 true라면, 절두체에서 far plane이나 near plane에서 잘려나간 텍셀이 있다면 그것을 자르지 않고 그대로 fragment shader에 흘려보낸다.

rasterizaerDiscardEnable은 rasterizer를 끄고 키는데 사용된다. 만약 true가 된다면 rasterizer가 실행되지 않고 텍셀이 생성되지 않는다.

polygonMode는 vulkan이 삼각형을 점이나 선으로 자동적으로 변경하는데 사용할 수 있다. 값은 다음과 같다.

  • VK_POLYGON_MODE_FILL : 이는 삼각형을 채우는데 사용되는 기본 방식이다. 삼각형 내부는 채워진 것으로 그려지며, 삼각형 안의 모든 점은 텍셀을 생성한다.
  • VK_POLYGON_MODE_LINE : 삼각형을 선으로 바꾸며, 각 삼각형의 모서리가 선이 된다. 이는 와이어프레임 방식으로 그릴때 유용하다.
  • VK_POLYGON_MODE_POINT : 각 vertex를 점으로 그린다.

culling은 cullMode로 설정되며, 비트 단위의 조합이거나 0이 가능하다.

  • VK_CULL_MODE_FRONT_BIT : 카메라를 바라보는 다각형은 버려진다.
  • VK_CULL_MODE_BACK_BIT : 카메라를 바라보지 않은 (뒷면) 다각형은 버려진다.
  • VK_CULL_MODE_FRONT_AND_BACK : VK_CULL_MODE_FRONT_BITVK_CULL_MODE_BACK_BITor 연산 한 비트이다. 이 값으로 설정하면 모든 다각형이 버려진다.

culling은 winding order로 정해진다는 것을 기억해두자. 

dx와는 다르게, vulkan은 '시계 방향'의 감기 순서를 가진다.

depthBiasEnable, depthBiasConstantFactor, depthBiasClamp, depthBiasSlopeFactor는 깊이 보정 기능을 조절한다. 이 기능은 텍셀이 depth test 전에 오프셋을 가지에 하여 depth fighting을 제거하는 데 사용한다. 나중에 따로 다룬다.

lineWidth는 line primitive의 너비를 설정하는 것이다. 1.0 초과의 값을 설정하면 느려질 수도 있다는 것을 알아두자.

 

다음은 본인의 코드에서 rasterization state를 설정하는 코드이다.

configInfo.rasterizationInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
configInfo.rasterizationInfo.depthClampEnable = VK_FALSE;
configInfo.rasterizationInfo.rasterizerDiscardEnable = VK_FALSE;
configInfo.rasterizationInfo.polygonMode = VK_POLYGON_MODE_FILL;
configInfo.rasterizationInfo.lineWidth = 1.0f;
configInfo.rasterizationInfo.cullMode = VK_CULL_MODE_NONE;
configInfo.rasterizationInfo.frontFace = VK_FRONT_FACE_CLOCKWISE;
configInfo.rasterizationInfo.depthBiasEnable = VK_FALSE;
configInfo.rasterizationInfo.depthBiasConstantFactor = 0.0f;
configInfo.rasterizationInfo.depthBiasClamp = 0.0f;
configInfo.rasterizationInfo.depthBiasSlopeFactor = 0.0f;

4. Multi sampling

Multi Sampling은 이미지의 각 픽세렝 대한 샘플을 생성하는 과정이다.

이는 앨리어싱을 제거하는 데 사용될 수 있으며 효과적으로 사용될 경우 이미지 질을 크게 향상시킨다.

multi sampling을 사용할 때, color와 depth-stencil은 반드시 multi sampling 이미지여야 한다.

기본적으로, 색이 어떻게 칠해지느냐는 각 텍셀의 한 점을 대상으로 한다.

그 한 점이 primitive안에 포함된다면, 래스터라이저는 이 텍셀을 포함하여 Fragment shader에 넘겨주게 된다.

이렇게 되면, '조금 포함된' 텍셀은 벗어나게 되어 앨리어싱(계단현상)이 발생하게 되는데, 이를 MSAA(Multi sampling anti aliasing)로 해결해야 한다.

이 처럼 각 텍셀에 더 많은 점을 두어, 점이 얼마나 포함됐는가를 가지고 앨리어싱 현상을 피한다.

파이프라인의 multi sampling 상태는 VkPipelineMultisampleStateCreateInfo이며, 정의는 다음과 같다.

typedef struct VkPipelineMultisampleStateCreateInfo {
    VkStructureType                          sType;
    const void*                              pNext;
    VkPipelineMultisampleStateCreateFlags    flags;
    VkSampleCountFlagBits                    rasterizationSamples;
    VkBool32                                 sampleShadingEnable;
    float                                    minSampleShading;
    const VkSampleMask*                      pSampleMask;
    VkBool32                                 alphaToCoverageEnable;
    VkBool32                                 alphaToOneEnable;
} VkPipelineMultisampleStateCreateInfo;

sTypeVK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO로 설정해야 하며

pNext는 nullptr여야 한다.

flags는 예비로 설정되었으며 0으로 설정돼야한다.

rasterizationSamples는 rasterization에 사용된 샘플수를 지정하는 값이다.

sampleShadingEnable은 Sample Shading을 활성화 하는데 사용된다.

minSampleShading은 Sample Shading의 최소 비율을 지정한다.

이 부분은, 추후 multi sampling에서 다시 다룬다.

 

다음은 본인의 코드에서 Multi sampling을 정의하는 코드이다. (아직은 disable 상태이다.)

configInfo.multisampleInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
configInfo.multisampleInfo.sampleShadingEnable = VK_FALSE;
configInfo.multisampleInfo.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
configInfo.multisampleInfo.minSampleShading = 1.0f;
configInfo.multisampleInfo.pSampleMask = nullptr;
configInfo.multisampleInfo.alphaToCoverageEnable = VK_FALSE;
configInfo.multisampleInfo.alphaToOneEnable = VK_FALSE;

5. Depth-stencil state

깊이-스텐실 상태는 깊이와 스텐실 테스트가 어떻게 수행되고, Fragment가 테스트에서 통과되거나 실패할 떄 어떻게 처리되는지를 조절한다.  

depth stencil test는 fragment가 실행되기 전이나 후에 실행될 수 있다. 기본적으로, test는 fragment shader 이후에 실행된다.

만약 fragment shader를 depth test 이전에 실행하기 위해서는, EarlyFragment Tests 방식을 적용할 수 있다.

VkPipelineDepthStencilStateCreateInfo의 정의는 다음과 같다.

typedef struct VkPipelineDepthStencilStateCreateInfo {
    VkStructureType                           sType;
    const void*                               pNext;
    VkPipelineDepthStencilStateCreateFlags    flags;
    VkBool32                                  depthTestEnable;
    VkBool32                                  depthWriteEnable;
    VkCompareOp                               depthCompareOp;
    VkBool32                                  depthBoundsTestEnable;
    VkBool32                                  stencilTestEnable;
    VkStencilOpState                          front;
    VkStencilOpState                          back;
    float                                     minDepthBounds;
    float                                     maxDepthBounds;
} VkPipelineDepthStencilStateCreateInfo;

sTypeVK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_CREATE_INFO로 설정돼야 하며,

pNext는 nullptr로 설정돼야 한다.

depthTestEnable은 depthTest가 실행될지를 결정한다. 만약 depthTest를 하기로 결정했다면, 시험은

depthCompareOp를 사용해서 test하며 이는 VkCompareOp enum값중 하나다. (이는 추후에 다시 다룬다)

depthTestEnable이 false라면 모든 Fragment들은 depth test가 통과된것으로 간주된다. 하지만 depth buffer에 어떠한 쓰기도 일어나지 않는다.

 

다음은 본인의 코드에서 depth-stencil state를 정의하는 코드이다.

configInfo.depthStencilInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
configInfo.depthStencilInfo.depthTestEnable = VK_TRUE;
configInfo.depthStencilInfo.depthWriteEnable = VK_TRUE;
configInfo.depthStencilInfo.depthCompareOp = VK_COMPARE_OP_LESS;
configInfo.depthStencilInfo.depthBoundsTestEnable = VK_FALSE;
configInfo.depthStencilInfo.minDepthBounds = 0.0f;
configInfo.depthStencilInfo.maxDepthBounds = 1.0f;
configInfo.depthStencilInfo.stencilTestEnable = VK_FALSE;
configInfo.depthStencilInfo.front = {};
configInfo.depthStencilInfo.back = {};

 

6. Color blend state

vulkan 그래픽 파이프라인의 마지막 단계는 color blending 단계이다. 이 단계에서 실제 Fragment를 프레임 버퍼에 쓰게 된다. 대부분은 단순한 연산으로, Fragment를 프레임 버퍼(렌더 타겟)에 쓰게되는데 (depth-test를 통하여) color blending은 이 값을 이미 프레임 버퍼에 있는 값과 섞어서 Fragment shader의 출력 값과 프레임 버퍼의 현재 값을 논리적 연산을 하여 섞을 수(blending) 있다. 

 

Blend state는 VkPipelineColorBlendStateCreateInfo를 사용해서 설정할 수 있다. 정의는 다음과 같다.

typedef struct VkPipelineColorBlendStateCreateInfo {
    VkStructureType                               sType;
    const void*                                   pNext;
    VkPipelineColorBlendStateCreateFlags          flags;
    VkBool32                                      logicOpEnable;
    VkLogicOp                                     logicOp;
    uint32_t                                      attachmentCount;
    const VkPipelineColorBlendAttachmentState*    pAttachments;
    float                                         blendConstants[4];
} VkPipelineColorBlendStateCreateInfo;

sTypeVK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO여야 하며,

pNext는 nullptr여야 한다.

flags는 예비를 위해 예약되었다.

logicOpEnable은 Fragment Shader의 결과와 프레임 버퍼에 적혀진 컬러 값과 논리연산을 수행할 지 결정한다.

만약 이것이 false라면 논리연산을 수행하지 않고 blend attachment에 변경되지 않고 쓰여진다.

이 내용은 추후에 다시 다룬다.

 

각 attachment는 다른 형식을 가질 수 있으며, 다른 혼합 연산을 지원하기 위해 사용된다.

VkPipelineColorBlendAttachmentState의 정의는 다음과 같다.

typedef struct VkPipelineColorBlendAttachmentState {
    VkBool32                 blendEnable;
    VkBlendFactor            srcColorBlendFactor;
    VkBlendFactor            dstColorBlendFactor;
    VkBlendOp                colorBlendOp;
    VkBlendFactor            srcAlphaBlendFactor;
    VkBlendFactor            dstAlphaBlendFactor;
    VkBlendOp                alphaBlendOp;
    VkColorComponentFlags    colorWriteMask;
} VkPipelineColorBlendAttachmentState;

각 attachment는 blendEnable 한지, 원본 소스(srcColor, srcAlpha)대상 소스(dstColor, dstAlpha)가 어떻게 되며,

어떻게 혼합 연산을 하는지 (colorBlendOp, aplhaBlendOp) 결정한다. 이 또한 추후에 다시 다룬다.

 

다음은 본인의 코드에서 Color blend state를 정의하는 코드이다.

configInfo.colorBlendInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
configInfo.colorBlendInfo.logicOpEnable = VK_FALSE;
configInfo.colorBlendInfo.logicOp = VK_LOGIC_OP_COPY;
configInfo.colorBlendInfo.attachmentCount = 1;
configInfo.colorBlendInfo.pAttachments = &configInfo.colorBlendAttachment;
configInfo.colorBlendInfo.blendConstants[0] = 0.0f;
configInfo.colorBlendInfo.blendConstants[1] = 0.0f;
configInfo.colorBlendInfo.blendConstants[2] = 0.0f;
configInfo.colorBlendInfo.blendConstants[3] = 0.0f;

logicOperator를 사용하지 않으므로, attachment는 따로 초기화하지 않았다.

 

이제 실제로 파이프라인을 만들어야 할 차례이다.

일단 기존의 컴파일한 셰이더 모듈을 가져와 정보를 작성해야한다.

정보를 작성할 구조체는 VkPipelineShaderStageCreateInfo이며 정의는 다음과 같다.

typedef struct VkPipelineShaderStageCreateInfo {
    VkStructureType                     sType;
    const void*                         pNext;
    VkPipelineShaderStageCreateFlags    flags;
    VkShaderStageFlagBits               stage;
    VkShaderModule                      module;
    const char*                         pName;
    const VkSpecializationInfo*         pSpecializationInfo;
} VkPipelineShaderStageCreateInfo;

sTypeVK_STRUCTURE_TYPE_PIPELINE__SHADER_STAGE_CREATE_INFO여야 하며,

pNext는 nullptr이다.

flags는 추후를 위해 예약되어 있다.

stage는 VkShaderStageFlagBits의 열거체이다.

module은 생성된 셰이더 모듈이다.

pName은 진입점의 이름이다.

pSpecializationInfo는 추후에 다시 작성한다.

 

다음은 컴파일한 셰이더 모듈 (vertex, fragment)의 정보를 작성하는 본인의 코드이다.

CreateShaderModule(vsCode, &vertShaderModule);	// vertex shader 모듈 생성
CreateShaderModule(fsCode, &fragShaderModule);	// fragment shader 모듈 생성

VkPipelineShaderStageCreateInfo shaderStages[2];
shaderStages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
shaderStages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
shaderStages[0].module = vertShaderModule;
shaderStages[0].pName = "main";
shaderStages[0].pNext = nullptr;
shaderStages[0].flags = 0;
shaderStages[0].pSpecializationInfo = nullptr;

shaderStages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
shaderStages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
shaderStages[1].module = fragShaderModule;
shaderStages[1].pName = "main";
shaderStages[1].pNext = nullptr;
shaderStages[1].flags = 0;
shaderStages[1].pSpecializationInfo = nullptr;

- 정점 입력 상태 ( 아직은 깊이 알 필요 없음, 추후에 다시 설명함 )

다음은 정점 입력 상태를 정의해야한다.

실제 기하구조를 렌더링하기위해서, vulkan 파이프라인의 입구에 자료를 넣어야 한다.

SPIR-V가 프로그래밍으로 생성한 기하구조나, 명시적으로 버퍼에서 기하구조 자료를 얻어서 제공된 vertex와 index를 사용할 수 있다. 아니면, 메모리의 기하구조 자료의 배치를 설정하여 vulkan이 이를 가져와서 직접 셰이더에 제공할 수 있다.

정점 입력 상태 정의는 VkPipelineVertexInputStateCreateInfo 로 정의하며, 정의는 다음과 같다.

typedef struct VkPipelineVertexInputStateCreateInfo {
    VkStructureType                             sType;
    const void*                                 pNext;
    VkPipelineVertexInputStateCreateFlags       flags;
    uint32_t                                    vertexBindingDescriptionCount;
    const VkVertexInputBindingDescription*      pVertexBindingDescriptions;
    uint32_t                                    vertexAttributeDescriptionCount;
    const VkVertexInputAttributeDescription*    pVertexAttributeDescriptions;
} VkPipelineVertexInputStateCreateInfo;

sTypeVK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO 여야 하며,

pNext는 nullptr이다.

flags는 예비로 예약되어 있다.

 

정점 입력 상태는 자료를 포함한 버퍼를 연결할 수 있는 정점 연결의 집합과, 이 버퍼에서 정점 자료가 어떻게 배치되는지 설명하는 정점 특성의 집합으로 나누어 진다.

정점 버퍼 연결 점에 연결된 버퍼는 종종 정점 버퍼로 참조된다. 어떤 버퍼도 정점 자료를 저장할 수 있기에 정점 버퍼 같은 것이 실제로는 없으며, 단일 버퍼는 정점 자료와 다른 자료도 역시 저장할 수 있다.

정점 버퍼로 사용되는 버퍼의 유일한 요구사항은 반드시 VK_BUFFER_USAGE_VERTEX_BUFFER_BIT를 설정해서 사용해야한다는 것이다.

vertexBindingDescriptionCount는 파이프라인에서 사용될 vertexBindingDescriptions의 갯수이며,

pVertexBindingDescriptions는 해당 수만큼의 VkVertexInputBindingDescription 구조체의 배열에 대한 포인터이다.

정의는 다음과 같다.

typedef struct VkVertexInputBindingDescription {
    uint32_t binding;
    uint32_t stride;
    VkVertexInputRate inputRate;
} VkVertexInputBindingDescription;

binding 항목은 이 구조체에서 설정한 연결의 index 이다. 각 파이프라인은 정점 버퍼 연결의 수를 알려줄 수 있으며, 이의 index는 연속적일 필요가 없다. 파이프라인에서 사용되는 모든 연결이 설정되어 있기만 한다면 주어진 파이프라인에서 모든 연결을 설명할 필요는 없다.

stride는 새로운 정점 버퍼가 몇 바이트 후에 다시 시작되는지를 알려주는 요소이다.

만약 100byte로 나뉘어져 새로운 정점버퍼가 나온다면, stride는 100이다.

이 정점버퍼는 index나 instancing을 사용하여 배열을 순회할 수 있다.

이때는 inputRate가 VK_VERTEX_INPUT_RATE_VERTEX이거나 VK_VERTEX_INPUT_RATE_INSTANCE 이다.

 

각 정점 특성은 본질적으로 정점 버퍼에 저장된 구조체 중 하나의 구성원이다.

정점 버퍼에서의 각 정점 특성은 단계 비율과 배열의 폭을 공유하지만, 고유의 자료 형과 해당 구조체의 오프셋을 가진다.

이는 VkVertexInputAttributeDescription 구조체를 사용해서 설정된다.

정의는 다음과 같다.

typedef struct VkVertexInputAttributeDescription {
    uint32_t    location;
    uint32_t    binding;
    VkFormat    format;
    uint32_t    offset;
} VkVertexInputAttributeDescription;

location은 vertex input에서 어느 위치에 존재할 것인가를 정의한다.

binding은 위의 VkVertexInputBindingDescription 구조체의 배열에 설정된 연결 중 하나와 일치해야 한다.

format은 현재 연결된 정점이 어떠한 포맷으로 나타나야 하는가를 정의한다.

offset은 각 구조체의 offset을 결정한다.

 

다음은 VkVertexInputBindingDescriptionVkVertexInputAttritubeDescription을 사용하는 코드이다.

static const VkVertexInputBindingDescription vertexInputBindings[] =
{
    { 0, sizeof(vertex), VK_VERTEX_INPUT_RATE_VERTEX }	// BUFFER
};

static const VkVertexInputAttributeDescription vertexAttributes[] =
{
    { 0, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 0 }, // position
    { 1, 0, VK_FLOAT_R32G32B32_SFLOAT, offsetof(vertex, normal) }, // normal
    { 2, 0, VK_FORMAT_R32G32_SFLOAT, offsetof(vertex, texcoord) } //TexCoord
}
directx12를 해본 사람이라면 알 것이다.
VkVertexInputBindingDescription은 vertexbuffer view
VkVertexInputAttributeDescription 은 inputlayout과 매우 유사하다.

 

다음은 본인의 코드에서 정점 입력상태를 나타내는 코드이다.

VkPipelineVertexInputStateCreateInfo vertexInputInfo{};
vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
vertexInputInfo.vertexBindingDescriptionCount = 0;
vertexInputInfo.vertexAttributeDescriptionCount = 0;
vertexInputInfo.pVertexAttributeDescriptions = nullptr;
vertexInputInfo.pVertexBindingDescriptions = nullptr;

우리는 셰이더에 작성된 vertex로만 삼각형을 나타낼 것이며, 정점 입력부분은 추후에 다시 다룬다.


이제 실제로 파이프라인을 생성해야 한다.

파이프라인을 생성할 때, 정보들을 정의하는 것은 VkGraphicsPipelineCreateInfo를 통해 정의한다.

정의는 다음과 같다.

typedef struct VkGraphicsPipelineCreateInfo {
    VkStructureType                                  sType;
    const void*                                      pNext;
    VkPipelineCreateFlags                            flags;
    uint32_t                                         stageCount;
    const VkPipelineShaderStageCreateInfo*           pStages;
    const VkPipelineVertexInputStateCreateInfo*      pVertexInputState;
    const VkPipelineInputAssemblyStateCreateInfo*    pInputAssemblyState;
    const VkPipelineTessellationStateCreateInfo*     pTessellationState;
    const VkPipelineViewportStateCreateInfo*         pViewportState;
    const VkPipelineRasterizationStateCreateInfo*    pRasterizationState;
    const VkPipelineMultisampleStateCreateInfo*      pMultisampleState;
    const VkPipelineDepthStencilStateCreateInfo*     pDepthStencilState;
    const VkPipelineColorBlendStateCreateInfo*       pColorBlendState;
    const VkPipelineDynamicStateCreateInfo*          pDynamicState;
    VkPipelineLayout                                 layout;
    VkRenderPass                                     renderPass;
    uint32_t                                         subpass;
    VkPipeline                                       basePipelineHandle;
    int32_t                                          basePipelineIndex;
} VkGraphicsPipelineCreateInfo;

sType은 VK_STRUCTURE_TYPE_GRAPHGICS_PIPELINE_CREATE_INFO 여야 하며,

pNext는 nullptr이다.

flags는 예비로 지정되어 있다.

stageCount는 셰이더 스테이지의 갯수 이다.

pStages는 셰이더 모듈을 설명한 VkPipelineShaderStageCreateInfo의 포인터이다.

pVertexInputState는 정점 입력상태의 포인터이다.

pInputAssemblyState는 Input Assembly의 포인터이다.

pTessellationState는 테셀레이션 상태의 포인터이다. (이는 아직 정의하지 않았다. 추후 추가)

pViewportState는 뷰포트의 포인터이다.

pRasterizationState는 래스터라이저 상태의 포인터이다.

pMultisampleState는 multi sampling의 포인터이다.

pDepthStencilState는 depth-stencil 상태의 포인터이다.

pColorBlendState는 color-blend 상태의 포인터이다.

pDynamicState 는 동적상태의 포인터이다.

pDynamicState, layout, renderPass, subpass, basePipelineHandle, basePipelineIndex 는 다음 글에서 설명한다.

 

다음은 본인 코드에서 모든 상태 정보를 포함한 그래픽 파이프라인을 생성하는 과정이다.

VkGraphicsPipelineCreateInfo pipelineInfo{};
pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
pipelineInfo.stageCount = 2;
pipelineInfo.pStages = shaderStages;
pipelineInfo.pVertexInputState = &vertexInputInfo;
pipelineInfo.pInputAssemblyState = &configInfo.inputAssemblyInfo;
pipelineInfo.pViewportState = &configInfo.viewportInfo;
pipelineInfo.pRasterizationState = &configInfo.rasterizationInfo;
pipelineInfo.pColorBlendState = &configInfo.colorBlendInfo;
pipelineInfo.pDepthStencilState = &configInfo.depthStencilInfo;
pipelineInfo.pMultisampleState = &configInfo.multisampleInfo;
pipelineInfo.pDynamicState = nullptr;

pipelineInfo.layout = configInfo.pipelineLayout;
pipelineInfo.renderPass = configInfo.renderPass;
pipelineInfo.subpass = configInfo.subPass;

pipelineInfo.basePipelineIndex = -1;
pipelineInfo.basePipelineHandle = VK_NULL_HANDLE;

if (vkCreateGraphicsPipelines(device.GetDevice(), VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &graphicsPipeline)) {
	throw std::runtime_error{ "failed to create graphics pipeline" };
}

실제로 이상태로 컴파일을 해보면

validation layer가 다음과 같은 오류를 발생시키게 된다.

이렇게 validation layer를 통해서 어떤게 문제인질 알아낼 수 있다.

현재 코드로 봐서는

layout, renderpass가 nullptr 이라고 오류를 낸다.

 

모든 오류와 설명은 다음 글에서 설명할 것이다.