-
Variable Shading Rate(VRS)Graphics/기본 2022. 9. 27. 00:08
목차
1. 개요
2. 내용
2.1. 디바이스 생성
2.2. VRS 이미지 생성
2.3. VRS 사용을 위한 Graphics Pipeline State 생성
2.4. VRS 디버깅을 위한 코드 추가
2.5. 구현결과2.6. 구현 코드
3. 레퍼런스1. 개요
Variable Shading Rate(VRS) 에 대해서 알아보고 구현해봅시다.
VRS 는 화면의 영역별로 쉐이딩 비율을 조정하는 기능입니다. 그림1을 보면, 1x1 부터 4x4 까지 다양한 쉐이딩 비율이 있습니다. 1x1 이 VRS 를 사용하지 않은 기본 비율입니다. 그리고 2x2 는 4픽셀당 1번 쉐이딩을 합니다.
이 기능을 활용하여 사람의 시야에서 인지적으로 크게 영향을 미치지 않는 부분는 더 적은 수의 픽셀을 쉐이딩 하여 퍼포먼스를 얻을 수 있을 것입니다. 그림2에 보는바와 같이 이 기능은 VR에서 활용도가 높을 것입니다.
2. 내용
VRS 의 구현은 간단합니다. 화면의 쉐이딩 비율을 저장할 이미지를 하나 생성합니다. 그리고 그 이미지에 각 스크린에 맞는 쉐이딩 비율 정보를 기록합니다. VRS 의 구현은 Vulkan API 를 기준으로 알아볼 것입니다.
2.1. 디바이스 생성
VRS 를 사용하려면 VkDevice 를 생성할 때, VkPhysicalDeviceShadingRateImageFeaturesNV 를 추가해줘야 합니다.
... VkDeviceCreateInfo createInfo = {}; createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; ... // VRS 기능 활성 VkPhysicalDeviceShadingRateImageFeaturesNV enabledPhysicalDeviceShadingRateImageFeaturesNV{}; enabledPhysicalDeviceShadingRateImageFeaturesNV.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADING_RATE_IMAGE_FEATURES_NV; enabledPhysicalDeviceShadingRateImageFeaturesNV.shadingRateImage = VK_TRUE; physicalDeviceFeatures2.pNext = &enabledPhysicalDeviceShadingRateImageFeaturesNV; createInfo.pNext = &physicalDeviceFeatures2; ... if (!ensure(vkCreateDevice(PhysicalDevice, &createInfo, nullptr, &Device) == VK_SUCCESS)) return false; ...
2.2. VRS 이미지 생성
쉐이딩 비율을 설정은 Enum 을 이미지에 기록하여 픽셀당 쉐이딩 비율을 기록합니다.
// 쉐이딩 비율을 나타내는 Enum typedef enum VkShadingRatePaletteEntryNV { ... VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_PIXEL_NV = 5, VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_2X1_PIXELS_NV = 6, VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_1X2_PIXELS_NV = 7, VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_2X2_PIXELS_NV = 8, VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_4X2_PIXELS_NV = 9, VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_2X4_PIXELS_NV = 10, VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_4X4_PIXELS_NV = 11, ... } VkShadingRatePaletteEntryNV;
쉐이딩 비율은 shadingRateTexelSize 당 하나씩 설정됩니다.
// 쉐이딩 비율을 physicalDeviceShadingRateImagePropertiesNV.shadingRateTexelSize 당 설정됩니다. // 제가 사용하는 RTX3070 에서는 (16, 16) 픽셀당 쉐이딩 비율을 설정합니다. VkExtent3D imageExtent{}; imageExtent.width = static_cast<uint32_t>(ceil(SCR_WIDTH / (float)physicalDeviceShadingRateImagePropertiesNV.shadingRateTexelSize.width)); imageExtent.height = static_cast<uint32_t>(ceil(SCR_HEIGHT / (float)physicalDeviceShadingRateImagePropertiesNV.shadingRateTexelSize.height)); imageExtent.depth = 1;
레퍼런스3 을 참고하여 VRS 에 사용할 이미지를 아래와 같이 만들었습니다. 실제로 사용하게 되면 Compute shader 를 사용하여 리얼타임에서 계속해서 변경하게 만들 것 입니다만 여기서는 간단히 변하지 않는 VRS 텍스쳐를 사용합니다.
jTexture* jRHI_Vulkan::CreateSampleVRSTexture() { if (ensure(!SampleVRSTexture)) { VkPhysicalDeviceShadingRateImagePropertiesNV physicalDeviceShadingRateImagePropertiesNV{}; physicalDeviceShadingRateImagePropertiesNV.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADING_RATE_IMAGE_PROPERTIES_NV; VkPhysicalDeviceProperties2 deviceProperties2{}; deviceProperties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; deviceProperties2.pNext = &physicalDeviceShadingRateImagePropertiesNV; vkGetPhysicalDeviceProperties2(g_rhi_vk->PhysicalDevice, &deviceProperties2); VkExtent3D imageExtent{}; imageExtent.width = static_cast<uint32_t>(ceil(SCR_WIDTH / (float)physicalDeviceShadingRateImagePropertiesNV.shadingRateTexelSize.width)); imageExtent.height = static_cast<uint32_t>(ceil(SCR_HEIGHT / (float)physicalDeviceShadingRateImagePropertiesNV.shadingRateTexelSize.height)); imageExtent.depth = 1; auto NewVRSTexture = new jTexture_Vulkan(); jVulkanBufferUtil::CreateImage(imageExtent.width, imageExtent.height, 1, (VkSampleCountFlagBits)1, GetVulkanTextureFormat(ETextureFormat::R8UI), VK_IMAGE_TILING_OPTIMAL , VK_IMAGE_USAGE_SHADING_RATE_IMAGE_BIT_NV | VK_IMAGE_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, VK_IMAGE_LAYOUT_UNDEFINED, *NewVRSTexture); VkDeviceSize imageSize = imageExtent.width * imageExtent.height * GetVulkanTextureComponentCount(ETextureFormat::R8UI); jBuffer_Vulkan stagingBuffer; jVulkanBufferUtil::CreateBuffer(VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT , imageSize, stagingBuffer); VkCommandBuffer commandBuffer = g_rhi_vk->BeginSingleTimeCommands(); ensure(g_rhi_vk->TransitionImageLayout(commandBuffer, (VkImage)NewVRSTexture->GetHandle(), GetVulkanTextureFormat(ETextureFormat::R8UI), 1, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)); jVulkanBufferUtil::CopyBufferToImage(commandBuffer, stagingBuffer.Buffer, (VkImage)NewVRSTexture->GetHandle() , static_cast<uint32>(imageExtent.width), static_cast<uint32>(imageExtent.height)); // Create a circular pattern with decreasing sampling rates outwards (max. range, pattern) std::map<float, VkShadingRatePaletteEntryNV> patternLookup = { { 1.5f * 8.0f, VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_PIXEL_NV }, { 1.5f * 12.0f, VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_2X1_PIXELS_NV }, { 1.5f * 16.0f, VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_1X2_PIXELS_NV }, { 1.5f * 18.0f, VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_2X2_PIXELS_NV }, { 1.5f * 20.0f, VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_4X2_PIXELS_NV }, { 1.5f * 24.0f, VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_2X4_PIXELS_NV } }; VkDeviceSize bufferSize = imageExtent.width * imageExtent.height * sizeof(uint8); uint8_t val = VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_4X4_PIXELS_NV; uint8_t* shadingRatePatternData = new uint8_t[bufferSize]; memset(shadingRatePatternData, val, bufferSize); uint8_t* ptrData = shadingRatePatternData; for (uint32_t y = 0; y < imageExtent.height; y++) { for (uint32_t x = 0; x < imageExtent.width; x++) { const float deltaX = (float)imageExtent.width / 2.0f - (float)x; const float deltaY = ((float)imageExtent.height / 2.0f - (float)y) * ((float)SCR_WIDTH / (float)SCR_HEIGHT); const float dist = std::sqrt(deltaX * deltaX + deltaY * deltaY); for (auto pattern : patternLookup) { if (dist < pattern.first) { *ptrData = pattern.second; break; } } ptrData++; } } check(imageSize == bufferSize); stagingBuffer.UpdateBuffer(shadingRatePatternData, bufferSize); g_rhi_vk->EndSingleTimeCommands(commandBuffer); stagingBuffer.Release(); delete[]shadingRatePatternData; NewVRSTexture->View = jVulkanBufferUtil::CreateImageView((VkImage)NewVRSTexture->GetHandle(), GetVulkanTextureFormat(NewVRSTexture->Format) , VK_IMAGE_ASPECT_COLOR_BIT, 1); SampleVRSTexture = NewVRSTexture; } return SampleVRSTexture; }
2.3. VRS 사용을 위한 Graphics Pipeline State 생성
마지막으로 렌더링하기 위해 필요한 PipelineState 생성 시 어떤 쉐이딩 비율이 사용될지 각각의 뷰포트에 설정해줍니다.
void* jPipelineStateInfo_Vulkan::CreateGraphicsPipelineState() { ... VkPipelineViewportStateCreateInfo viewportState = {}; viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; ... static const std::vector<VkShadingRatePaletteEntryNV> shadingRatePaletteEntries = { VK_SHADING_RATE_PALETTE_ENTRY_NO_INVOCATIONS_NV, VK_SHADING_RATE_PALETTE_ENTRY_16_INVOCATIONS_PER_PIXEL_NV, VK_SHADING_RATE_PALETTE_ENTRY_8_INVOCATIONS_PER_PIXEL_NV, VK_SHADING_RATE_PALETTE_ENTRY_4_INVOCATIONS_PER_PIXEL_NV, VK_SHADING_RATE_PALETTE_ENTRY_2_INVOCATIONS_PER_PIXEL_NV, VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_PIXEL_NV, VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_2X1_PIXELS_NV, VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_1X2_PIXELS_NV, VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_2X2_PIXELS_NV, VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_4X2_PIXELS_NV, VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_2X4_PIXELS_NV, VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_4X4_PIXELS_NV, }; if (PipelineStateFixed->IsUseVRS) { // VRS 를 사용하는 경우 VRS 이미지가 사용하는 쉐이딩 비율 Enum 을 Viewport state 에 전달합니다. // Shading Rate Palette shadingRatePalette.shadingRatePaletteEntryCount = static_cast<uint32_t>(shadingRatePaletteEntries.size()); shadingRatePalette.pShadingRatePaletteEntries = shadingRatePaletteEntries.data(); pipelineViewportShadingRateImageStateCI.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_SHADING_RATE_IMAGE_STATE_CREATE_INFO_NV; pipelineViewportShadingRateImageStateCI.shadingRateImageEnable = VK_TRUE; pipelineViewportShadingRateImageStateCI.viewportCount = viewportState.viewportCount; pipelineViewportShadingRateImageStateCI.pShadingRatePalettes = &shadingRatePalette; // Viewport state 에 넣어 전달 viewportState.pNext = &pipelineViewportShadingRateImageStateCI; } ... // Pipeline state 생성 if (!ensure(vkCreateGraphicsPipelines(g_rhi_vk->Device, g_rhi_vk->PipelineCache, 1, &pipelineInfo, nullptr, &vkPipeline) == VK_SUCCESS)) { return nullptr; } ... }
2.4. VRS 디버깅을 위한 코드 추가
VRS 이미지 잘 적용되었는지 디버깅 하기 위해서 쉐이더를 아래와 같이 수정할 수 있습니다. 이 코드 또한 레퍼런스3 의 내용을 참고했습니다.
float4 main(VSOutput input , uint shadingRate : SV_ShadingRate // 현재 픽셀의 쉐이딩 비율 정보를 담고 있음 ) : SV_TARGET { ... const uint SHADING_RATE_PER_PIXEL = 0x0; const uint SHADING_RATE_PER_2X1_PIXELS = 6; const uint SHADING_RATE_PER_1X2_PIXELS = 7; const uint SHADING_RATE_PER_2X2_PIXELS = 8; const uint SHADING_RATE_PER_4X2_PIXELS = 9; const uint SHADING_RATE_PER_2X4_PIXELS = 10; // 쉐이딩 비율에 맞게 컬러를 출력해줍니다. switch (shadingRate) { case SHADING_RATE_PER_PIXEL: return color * float4(0.0, 0.8, 0.4, 1.0); case SHADING_RATE_PER_2X1_PIXELS: return color * float4(0.2, 0.6, 1.0, 1.0); case SHADING_RATE_PER_1X2_PIXELS: return color * float4(0.0, 0.4, 0.8, 1.0); case SHADING_RATE_PER_2X2_PIXELS: return color * float4(1.0, 1.0, 0.2, 1.0); case SHADING_RATE_PER_4X2_PIXELS: return color * float4(0.8, 0.8, 0.0, 1.0); case SHADING_RATE_PER_2X4_PIXELS: return color * float4(1.0, 0.4, 0.2, 1.0); default: return color * float4(0.8, 0.0, 0.0, 1.0); } ... }
2.5. 구현결과
2.6. 구현 코드
https://github.com/scahp/jEngine/tree/VariableShadingRate
3. 레퍼런스
1. VRWorks - Variable Rate Shading (VRS)
2. Easy VRS Integration with Eye Tracking
3. https://github.com/SaschaWillems/Vulkan
'Graphics > 기본' 카테고리의 다른 글
BCn Texture Compression Formats 정리 (0) 2023.04.21 Wave Intrinsics (0) 2022.09.27 Halfspace Fog 공식 유도 정리 - 흡수와 산란 응용 (2) 2022.03.22 SphereMap TwoMirrorBall - 360도 방향 커버 됨 (0) 2020.12.11 The Structure Buffer (0) 2020.09.04