Computer Graphics/Vulkan

Vulkan - 14. 점광원에 대한 그림자 적용 (shadow cubemap)

surkim 2025. 1. 26. 15:17

점광원에 대한 그림자 적용했다.

 

렌더패스에서 만든 이미지를 다른 렌더패스로 받아 쓰는 것은

directional light, spotlight 그림자 할 때 했기 때문에 어렵지 않았는데

이번에 받아올 이미지는 cubemap이었기 때문에

cubemap을 생성, 사용하기 위해서 어떤 작업을 했어야했나에 대해 써야겠다.

 

일단 cubemap을 생성하기위해

shader에서 layer를 변경해줘가며 그려야하는데

일단 나는 깊이맵을 직접가져와

다른 랜더패스에서 읽을 수 있게 바꿔서 사용하는 방법을 사용했고

그래서 vertex shader는 이렇고 fragment shader는 없다.

#version 450
#extension GL_ARB_shader_viewport_layer_array : enable

layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inNormal;
layout(location = 2) in vec2 inTexCoord;
layout(location = 3) in vec3 inTangent;

layout(binding = 0) uniform ShadowUniformBufferObject {
    mat4 proj;
    mat4 view[6];
    mat4 model;
} ubo;

layout(binding = 1) uniform LayerIndex {
    uint layerIndex;
} layerData;

void main() {
    gl_Layer = int(layerData.layerIndex);
    gl_Position = ubo.proj * ubo.view[gl_Layer] * ubo.model * vec4(inPosition, 1.0);
}

보면 layerIndex를 uniform으로 줘서 gl_Layer에서 layer를 바꿔가며 그리고 있다.

 

여기서 extension을 추가해야했는데

gl_layer가 내장 변수라 이걸 사용하기 위해서는 저 옵션을 켜줘야 하고

이것때문에 vulkan 버전을 1.0에서 1.2로 바꿨고, 확장 옵션을 등록해줘야했다.

 

void VulkanContext::createInstance() {
    if (enableValidationLayers && !checkValidationLayerSupport()) {
        throw std::runtime_error("validation layers requested, but not available!");
    }

    VkApplicationInfo appInfo{};
    appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
    appInfo.pApplicationName = "Hello Triangle";
    appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
    appInfo.pEngineName = "No Engine";
    appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
    appInfo.apiVersion = VK_API_VERSION_1_2;

    VkInstanceCreateInfo createInfo{};
    createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
    createInfo.pApplicationInfo = &appInfo;

    std::vector<const char*> extensions = getRequiredExtensions();
    createInfo.enabledExtensionCount = static_cast<uint32_t>(extensions.size());
    createInfo.ppEnabledExtensionNames = extensions.data();

    VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{};

    if (enableValidationLayers) {
        createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());
        createInfo.ppEnabledLayerNames = validationLayers.data();
        populateDebugMessengerCreateInfo(debugCreateInfo);
        createInfo.pNext = reinterpret_cast<VkDebugUtilsMessengerCreateInfoEXT*>(&debugCreateInfo);
    } else {
        createInfo.enabledLayerCount = 0;		
        createInfo.pNext = nullptr;
    }

    if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {
        throw std::runtime_error("failed to create instance!");
    }
}

인스턴스 생성시 버전을 1.2로 바꿔주고

 

const std::vector<const char *> deviceExtensions = {VK_KHR_SWAPCHAIN_EXTENSION_NAME, VK_EXT_SHADER_VIEWPORT_INDEX_LAYER_EXTENSION_NAME };

device확장에 VK_EXT_SHADER_VIEWPORT_INDEX_LAYER_EXTENSION_NAME 레이어 확장 추가 해주면 된다.

 

cubemap 이미지기 때문에 프레임버퍼와 이미지 생성시 옵션이 다른데

VkFramebufferCreateInfo framebufferInfo{};
framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
framebufferInfo.renderPass = renderPass;
framebufferInfo.attachmentCount = 1;
framebufferInfo.pAttachments = &depthImageView;
framebufferInfo.width = shadowMapSize;
framebufferInfo.height = shadowMapSize;
framebufferInfo.layers = 6;

이게 프레임버퍼 생성 옵션이고

여기서 중요한건 layers가 6개!

VkImageCreateInfo imageInfo{};
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageInfo.imageType = VK_IMAGE_TYPE_2D;
imageInfo.extent.width = width;
imageInfo.extent.height = height;
imageInfo.extent.depth = 1;
imageInfo.mipLevels = mipLevels;
imageInfo.arrayLayers = 6;
imageInfo.format = format;
imageInfo.tiling = tiling;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
imageInfo.usage = usage;
imageInfo.samples = numSamples;
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
imageInfo.flags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT;

이미지 생성 옵션도 layer가 6개이고

마지막 flags를 cube로 해줘야 cubemap이 만들어진다.

 

layout(binding = 6) uniform samplerCube shadowCubeMap[4];

uint getCubeFace(vec3 L) {
    vec3 absL = abs(L);
    uint faceIndex;

    if (absL.x > absL.y && absL.x > absL.z) {
        // +X or -X
        faceIndex = (L.x > 0.0) ? 0u : 1u;
    } else if (absL.y > absL.x && absL.y > absL.z) {
        // +Y or -Y
        faceIndex = (L.y > 0.0) ? 2u : 3u;
    } else {
        // +Z or -Z
        faceIndex = (L.z > 0.0) ? 4u : 5u;
    }

    return faceIndex;
}

if (lights[i].onShadowMap == 1) {
    float closestDepth = texture(shadowCubeMap[shadowMapIndex], -L).r; // -L: light 방향
    uint faceIndex = getCubeFace(-L);
    mat4 lightView = view[shadowMapIndex][faceIndex];
    mat4 lightProj = proj[shadowMapIndex];

    mat4 lightViewProj = lightProj * lightView;

    vec4 lightSpacePosition = lightViewProj * vec4(fragPosition, 1.0);
    float currentDepth = lightSpacePosition.z / lightSpacePosition.w;

    float bias = 0.005;
    shadowFactor = currentDepth - bias > closestDepth ? 0.0 : 1.0;  
    attenuation *= shadowFactor;
    shadowMapIndex++;
}

light pass의 fragment shader 중 일부이고

여기서 특별하다고 생각했던 것은 cubemap은 texture의 픽셀 값 뽑아올 때

원래는 xy좌표로 뽑아오는데

cubemap은 vec3으로 6면 중에서 바로 뽑아 올수있다.

 

가장 가까운 깊이, 즉 빛이 적용될 position은 cubemap texture 바로 뽑아 올 순 있지만

현재 계산되는 position의 깊이는 결국 어떤 면의 view를 써야할지 찾아야되서

getCubeFace로 어떤 view matrix 썼는지 찾아온 후

깊이를 계산해서 비교해 그림자를 판별한다.

 

결과는

 

 

이로써

렌더링 부분에서

과제에서 요구하는 거는 다했고

(3가지 광원, 그림자, pbr)

그림자 퀄티티 좀 높이고 엔진 ui만드는 쪽과 합류해야겠다.

 

여기까지 하면서 느낀점은

opengl로 느려야 2주면 될 거 같은 일을

결국 2달 정도 걸려서 하게 되었는데

물론 vulkan을 하나도 모르는 상태에서 시작한거라

공부하는데 시간을 더 오래썼다 해도

생산성이 opengl에 비해 확연히 떨어진다는 것은 너무 뼈저러게 느꼈다.

확실히 vulkan쓰는 곳이 많이 없다하는데 그 이유를 알 것 같다.

 

그래도 많은 옵션을 정할 수 있어 유연하다는 것과

무엇보다 최적화만 잘 된다면 사용자의 재량에 따라 gpu성능을 최대로 끌어낼 수 있다라는 점이

정말 매력적인 거 같다.

 

아직 기능 구현하느라

최적화 부분을 많이 놓쳐가며 하고 있는데

특히 vulkan을 알면 알 수록

내 코드에 있어 최적화할 부분이 보이는 느낌이라

시간이 된다면 아니면 과제 후 나중에라도

최적화 부분도 해야겠다.