어우 드디어 했다.
이번의 주 도전과제는 새로운 렌더패스를 만들어 거기서 만든 것을 가져오는 것이다!
하면서 큰 이슈 사항이 나왔던 것은 두가지 였는 데
그 중 하나는 이미지 레이어를 잘 맞춰주지 않아 문제가 생긴 것이다.
이번에 만든 renderpass의 결과물은 depthmap image 였고
이미지 생성 당시 옵션은
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_IMAGE_USAGE_SAMPLED_BIT 이다.
하나는 깊이 계산 때문이고 (shadowmap 렌더패스)
하나는 이 이미지를 써서 빛 계산을 해야 하기 때문이다. (deferred shading 렌더패스)
처음엔
만들 당시 옵션만 정해주면 자연스럽게 변환이 되는 줄 알았는데
이것도 전부 명시 해줘야 Vulkan은 알아듣는다.
그래서 렌더링 중 파이프라인 베리어라는 것을 만들어
렌더패스가 지나고 이 베리어를 통해 이미지 레이아웃을 변경해줘야 한다.
첫 번째 렌더패스의 베리어
VkImageMemoryBarrier barrierToShaderRead{};
barrierToShaderRead.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
barrierToShaderRead.oldLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL;
barrierToShaderRead.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
barrierToShaderRead.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
barrierToShaderRead.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
barrierToShaderRead.image = m_shadowMapFrameBuffers[shadowMapIndex]->getDepthImage();
barrierToShaderRead.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
barrierToShaderRead.subresourceRange.baseMipLevel = 0;
barrierToShaderRead.subresourceRange.levelCount = 1;
barrierToShaderRead.subresourceRange.baseArrayLayer = 0;
barrierToShaderRead.subresourceRange.layerCount = 1;
vkCmdPipelineBarrier(
commandBuffer,
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
0,
0, nullptr,
0, nullptr,
1, &barrierToShaderRead
);
뎁스 계산용에서 읽기 전용으로 바꿔준다.
두번째 렌더패스의 베리어
VkImageMemoryBarrier barrierToDepthWrite{};
barrierToDepthWrite.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
barrierToDepthWrite.oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
barrierToDepthWrite.newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
barrierToDepthWrite.srcAccessMask = VK_ACCESS_SHADER_READ_BIT;
barrierToDepthWrite.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
barrierToDepthWrite.image = m_shadowMapFrameBuffers[i]->getDepthImage();
barrierToDepthWrite.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
barrierToDepthWrite.subresourceRange.baseMipLevel = 0;
barrierToDepthWrite.subresourceRange.levelCount = 1;
barrierToDepthWrite.subresourceRange.baseArrayLayer = 0;
barrierToDepthWrite.subresourceRange.layerCount = 1;
vkCmdPipelineBarrier(
commandBuffer,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT,
0,
0, nullptr,
0, nullptr,
1, &barrierToDepthWrite
);
셰이더에서 읽는 걸 끝냈으니
다음 프레임을 위해 이미지를 다시 깊이 계산용으로 돌려놓는다.
중요한 포인트는
이 옵션들 렌더패스와도 정확하게 일치해야한다.
첫번째 렌더패스는 이 이미지를 어태치먼트로 써야하기 때문에
렌더패스 생성시 어태치먼트 옵션도 저거와 동일하게 맞춰줘야한다.
렌더패스 생성 코드 중 일부
VkAttachmentDescription depthAttachment{};
depthAttachment.format = VulkanUtil::findDepthFormat();
depthAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
depthAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
depthAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
depthAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
depthAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
depthAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
depthAttachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL;
저기서 initialLayout과 finalLayout을 잘 맞춰줘야 한다.
나는 당근 Vulkan초보자라
렌더패스 여러 개는 처음이니깐 렌더링이 잘 안되서
다른게 문제 인 줄 알아서 한참 고치다가 알게 되었다.
이거는 다른 렌더패스의 생성 이미지를 가져와야하는데에 있어서 필수적인 거라
이제 알았으니 나중에 더 잘쓰면 되지!
다른 한 이슈는
셰이더에 배열을 넘겨줬는데 값이 계속 안들어오는 이슈가 있었다.
이 문제의 원인은
vulkan의 std140규칙 위반이었다.
이 규칙은 Vulkan및 glsl에서 사용하는 메모리 정렬 규칙인데
자세히 말해
스칼라는 4bytes, vec2는 8bytes, vec3,4는 16bytes로 정렬이 되어 읽는다는 규칙이다.
자세한건 https://docs.vulkan.org/guide/latest/shader_memory_layout.html 여기 참고
여기서 발생한 이슈는 배열인데
배열은 안에 무슨 값이든 16bytes씩 읽어서
내가 준 uint 배열또한 gpu에서 16bytes씩 읽고
나는 uint 배열을 cpu에서 4bytes씩 값을 넣었으니 여기에 대한 불균형 때문에 일어난 일이었다.
극약처방으로
내가 준 배열은 4개의 인덱스였는데
구조체 선언할 때 16개로 만든 후 4개 인덱스만큼 띄어서 주다가
동료가 이러면 메모리 손해라고 해서 기각되고
아예 배열을 안쓰는 로직으로 바꿔버렸다.
쨋든 해결완료! 또 하나 배웠다.
결과는
지금 광원은 5개이고 잘 보면 왼쪽 위 광원은 그림자 적용이 안된다.
왜냐면 그림자가 적용되는 광원은 4개로 한정지었기 때문이다.
실시간 그림자는 shadowmap그림자를 계속 만들어 넘겨줘야 하는데
2048 x 2048 기준
VK_FORMAT_D32_SFLOAT 옵션의 depthmap image이니깐 한 픽셀이 4bytes고
그럼 shadowmap 하나당 16MB이다.
근데 이 광원이 점 광원이 될 수도 있기 때문에 6개의 shadowmap 공간도 필요하니깐
광원당 16MB * (1 + 6) = 112MB를 사용하는데
내가 애당초 지원하려는 광원 수는 16개였었고
그럼 1792MB를 쓰게되는데
이건 말이 안된다.
그래서 그림자 적용은 4개로 한정지었고
광원 수는 대신 늘려도 되겠다.
로직은 그림자 체크 되어있는 것 중 가장 앞에 있는 그림자를 적용되게 했다.
16개 체크되어있어도
앞에 4개만 그림자가 적용이 된다.
좀 더 느낌있게 하려면 씬에 보이는 빛 중 4개를 적용되게 하면 좋을 거 같은데
이건 나중에 해봐야겠다.
결과 동영상!
영상 보면 앞의 그림자 적용 체크가 해제되면
뒤에 그림자가 적용되는 것을 볼 수 있다.
마지막에 그림자가 우는 게 보이는데
이거는 점광원 적용 후 한방에 고쳐야겠다.
다음은 점광원의 그림자 적용이다.
이거만 하면 다른 동료들과 합치게 된다.
현재 UI, 애니메이션, 물리엔진하고 있는 동료들 있는데
합치면 볼만 하겠다 ㅎㅎ
'Computer Graphics > Vulkan' 카테고리의 다른 글
Vulkan Game Engine - 15. UI 이식 (1) | 2025.02.03 |
---|---|
Vulkan - 14. 점광원에 대한 그림자 적용 (shadow cubemap) (0) | 2025.01.26 |
Vulkan - 12. 다중 광원 적용, 광원 타입 적용 (0) | 2025.01.15 |
Vulkan - 11. gltf 형식의 모델 로드, normal map 수정 (0) | 2025.01.14 |
Vulkan - 10. pbr 적용 (0) | 2025.01.07 |