Computer Graphics/Vulkan

Vulkan - 13. shadow map 생성, 다중 그림자 적용 (Spot light, Directional light)

surkim 2025. 1. 22. 16:16

어우 드디어 했다. 

이번의 주 도전과제는 새로운 렌더패스를 만들어 거기서 만든 것을 가져오는 것이다!

 

하면서 큰 이슈 사항이 나왔던 것은 두가지 였는 데

그 중 하나는 이미지 레이어를 잘 맞춰주지 않아 문제가 생긴 것이다.

 

이번에 만든 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, 애니메이션, 물리엔진하고 있는 동료들 있는데

합치면 볼만 하겠다 ㅎㅎ