ashen-aetna-ko

##잿빛 에트나

— 화산재로 덮인 화산 위에서 서툴게 헤매기

(3D 그래픽스, Rust, Vulkan, ash에 관한 튜토리얼)

서로의 뒤에: 깊이

이번에는 다음 두 개의 큐브를 사용해 봅시다:

        cube.insert_visibly(InstanceData {
            modelmatrix: (na::Matrix4::new_translation(&na::Vector3::new(0.0, 0.0, 0.1))
                * na::Matrix4::new_scaling(0.1))
            .into(),
            colour: [0.2, 0.4, 1.0],
        });
        cube.insert_visibly(InstanceData {
            modelmatrix: (na::Matrix4::new_translation(&na::Vector3::new(0.05, 0.05, 0.0))
                * na::Matrix4::new_scaling(0.1))
            .into(),
            colour: [1.0, 1.0, 0.2],
        });

두 큐브는 겹쳐져 있고, 파란색 큐브가 약간 더 뒤에 있습니다. 사진에서도 정확히 그렇게 보입니다. 좋습니다. 한번 다른 시도를 해보죠:

        cube.insert_visibly(InstanceData {
            modelmatrix: (na::Matrix4::new_translation(&na::Vector3::new(0.05, 0.05, 0.0))
                * na::Matrix4::new_scaling(0.1))
            .into(),
            colour: [1.0, 1.0, 0.2],
        });
        cube.insert_visibly(InstanceData {
            modelmatrix: (na::Matrix4::new_translation(&na::Vector3::new(0.0, 0.0, 0.1))
                * na::Matrix4::new_scaling(0.1))
            .into(),
            colour: [0.2, 0.4, 1.0],
        });

이제 파란색 상자가 다른 상자 앞에 그려집니다. 하지만 좌표상으로는 여전히 더 뒤에 있어야 합니다. 좌표는 중요하지 않고, 그리는 순서가 중요하다고요?! 이상하네요. 무슨 일일까요?

이 정점들을 그래픽 파이프라인으로 보낼 때 무슨 일이 일어날까요? 정점 셰이더(vertex shader)는 실제 좌표를 계산하고, 이 좌표들은 “화면의 이 지점을 칠해야 한다”는 명령어 집합으로 변환됩니다. 마지막으로 프래그먼트 셰이더(fragment shader)가 이 칠하기 작업을 수행합니다.

이 과정 어디에도 “이 점이 다른 점 뒤에 가려져 있는가? 그렇다면 그리지 말 것”과 같은 확인 절차가 포함되어 있지 않습니다. 우리는 이런 확인 절차를 추가해야 합니다.

각 프래그먼트를 그릴 때 z-좌표를 저장하고, 이전에 그려진 객체에 가려지지 않은 경우에만 칠하는 기능을 추가할 수 있습니다. 이 ‘기능’이 바로 뎁스 버퍼(depth buffer)입니다.

우리는 뎁스 버퍼에 렌더링할 것이므로, 렌더 패스(render pass) 중에 이 버퍼가 필요합니다. 이는 우리가 설정했던 프레임버퍼(framebuffer)와 관련이 있어 보입니다. 거기에도 ImageImageView가 포함되었었죠. 여기서도 비슷한 것이 필요할 겁니다.

스왑체인(swapchain)의 경우, 색상 렌더링을 위한 Image들은 자동으로 설정되었습니다. (스왑체인을 생성한 후 이 이미지들을 가져올 수 있었습니다.) 뎁스 이미지는 우리가 직접 생성해야 합니다.

이것을 다른 스왑체인 관련 항목들과 함께 넣어봅시다:

struct SwapchainDongXi {
    swapchain_loader: ash::extensions::khr::Swapchain,
    swapchain: vk::SwapchainKHR,
    images: Vec<vk::Image>,
    imageviews: Vec<vk::ImageView>,
    depth_image: vk::Image,                              
    depth_image_allocation: vk_mem::Allocation,          
    depth_image_allocation_info: vk_mem::AllocationInfo, 
    depth_imageview: vk::ImageView,                      
    framebuffers: Vec<vk::Framebuffer>,
    surface_format: vk::SurfaceFormatKHR,
    extent: vk::Extent2D,
    image_available: Vec<vk::Semaphore>,
    rendering_finished: Vec<vk::Semaphore>,
    may_begin_drawing: Vec<vk::Fence>,
    amount_of_images: u32,
    current_image: usize,
}

SwapChainDongXi::init() 내부에서, 먼저 이미지를 생성합니다.

        let extent3d = vk::Extent3D {
            width: extent.width,
            height: extent.height,
            depth: 1,
        };
        let depth_image_info = vk::ImageCreateInfo::builder()
            .image_type(vk::ImageType::TYPE_2D)
            .format(vk::Format::D32_SFLOAT)
            .extent(extent3d)
            .mip_levels(1)
            .array_layers(1)
            .samples(vk::SampleCountFlags::TYPE_1)
            .tiling(vk::ImageTiling::OPTIMAL)
            .usage(vk::ImageUsageFlags::DEPTH_STENCIL_ATTACHMENT)
            .sharing_mode(vk::SharingMode::EXCLUSIVE)
            .queue_family_indices(&queuefamilies);
        let allocation_info = vk_mem::AllocationCreateInfo {
            usage: vk_mem::MemoryUsage::GpuOnly,
            ..Default::default()
        };
        let (depth_image, depth_image_allocation, depth_image_allocation_info) =
            allocator.create_image(&depth_image_info, &allocation_info)?;

image_type은 여전히 2D 이미지이고, format은 이름에 ‘D’가 포함된 형식입니다(‘depth’를 의미합니다 — 참고: 32비트 버전은 성능 저하가 따르므로, 더 높은 정밀도가 필요하지 않다면 실제 애플리케이션에서는 24비트가 더 합리적일 것입니다). 이 함수에는 이미 스왑체인 이미지의 너비/높이인 extent가 있는데, 여기서는 너비, 높이, 깊이가 필요합니다. 밉매핑(mipmapping)은 없고, 레이어는 하나, 멀티샘플링도 없고, 큐 패밀리도 하나만 사용합니다(SharingMode::EXCLUSIVE와 이전에 사용했던 그래픽 큐 패밀리만 담긴 배열인 &queuefamilies). 메모리는 GPU에만 상주해도 되므로, 우리가 접근할 필요는 없습니다.

일부 옵션들은 우리가 스왑체인 생성에 사용했던 것들과 매우 유사합니다.

그 다음, 해당하는 ImageView를 생성합니다:

        let subresource_range = vk::ImageSubresourceRange::builder()
            .aspect_mask(vk::ImageAspectFlags::DEPTH)
            .base_mip_level(0)
            .level_count(1)
            .base_array_layer(0)
            .layer_count(1);
        let imageview_create_info = vk::ImageViewCreateInfo::builder()
            .image(depth_image)
            .view_type(vk::ImageViewType::TYPE_2D)
            .format(vk::Format::D32_SFLOAT)
            .subresource_range(*subresource_range);
        let depth_imageview =
            unsafe { logical_device.create_image_view(&imageview_create_info, None) }?;

중요: 포맷은 위에서 사용한 것과 일치해야 합니다. 우리가 생성했던 다른 ImageView들과의 차이점은, 이제 COLOR가 아닌 DEPTH에 관심이 있다는 것입니다.

이러한 변경 사항은 함수가 약간 다른 인자를 필요로 한다는 것을 의미합니다:

    fn init(
        instance: &ash::Instance,
        physical_device: vk::PhysicalDevice,
        logical_device: &ash::Device,
        surfaces: &SurfaceDongXi,
        queue_families: &QueueFamilies,
        allocator: &vk_mem::Allocator,
    ) -> Result<SwapchainDongXi, Box<dyn std::error::Error>> {
        let surface_capabilities = surfaces.get_capabilities(physical_device)?;
        let extent = surface_capabilities.current_extent;
        let surface_present_modes = surfaces.get_present_modes(physical_device)?;
        let surface_format = *surfaces.get_formats(physical_device)?.first().unwrap();
        let queuefamilies = [queue_families.graphics_q_index.unwrap()];
        let swapchain_create_info = vk::SwapchainCreateInfoKHR::builder()
            .surface(surfaces.surface)
            .min_image_count(
                3.max(surface_capabilities.min_image_count)
                    .min(surface_capabilities.max_image_count),
            )
            .image_format(surface_format.format)
            .image_color_space(surface_format.color_space)
            .image_extent(extent)
            .image_array_layers(1)
            .image_usage(vk::ImageUsageFlags::COLOR_ATTACHMENT)
            .image_sharing_mode(vk::SharingMode::EXCLUSIVE)
            .queue_family_indices(&queuefamilies)
            .pre_transform(surface_capabilities.current_transform)
            .composite_alpha(vk::CompositeAlphaFlagsKHR::OPAQUE)
            .present_mode(vk::PresentModeKHR::FIFO);
        let swapchain_loader = ash::extensions::khr::Swapchain::new(instance, logical_device);
        let swapchain = unsafe { swapchain_loader.create_swapchain(&swapchain_create_info, None)? };
        let swapchain_images = unsafe { swapchain_loader.get_swapchain_images(swapchain)? };
        let amount_of_images = swapchain_images.len() as u32;
        let mut swapchain_imageviews = Vec::with_capacity(swapchain_images.len());
        for image in &swapchain_images {
            let subresource_range = vk::ImageSubresourceRange::builder()
                .aspect_mask(vk::ImageAspectFlags::COLOR)
                .base_mip_level(0)
                .level_count(1)
                .base_array_layer(0)
                .layer_count(1);
            let imageview_create_info = vk::ImageViewCreateInfo::builder()
                .image(*image)
                .view_type(vk::ImageViewType::TYPE_2D)
                .format(vk::Format::B8G8R8A8_UNORM)
                .subresource_range(*subresource_range);
            let imageview =
                unsafe { logical_device.create_image_view(&imageview_create_info, None) }?;
            swapchain_imageviews.push(imageview);
        }
        let extent3d = vk::Extent3D {
            width: extent.width,
            height: extent.height,
            depth: 1,
        };
        let depth_image_info = vk::ImageCreateInfo::builder()
            .image_type(vk::ImageType::TYPE_2D)
            .format(vk::Format::D32_SFLOAT)
            .extent(extent3d)
            .mip_levels(1)
            .array_layers(1)
            .samples(vk::SampleCountFlags::TYPE_1)
            .tiling(vk::ImageTiling::OPTIMAL)
            .usage(vk::ImageUsageFlags::DEPTH_STENCIL_ATTACHMENT)
            .sharing_mode(vk::SharingMode::EXCLUSIVE)
            .queue_family_indices(&queuefamilies);
        let allocation_info = vk_mem::AllocationCreateInfo {
            usage: vk_mem::MemoryUsage::GpuOnly,
            ..Default::default()
        };
        let (depth_image, depth_image_allocation, depth_image_allocation_info) =
            allocator.create_image(&depth_image_info, &allocation_info)?;
        let subresource_range = vk::ImageSubresourceRange::builder()
            .aspect_mask(vk::ImageAspectFlags::DEPTH)
            .base_mip_level(0)
            .level_count(1)
            .base_array_layer(0)
            .layer_count(1);
        let imageview_create_info = vk::ImageViewCreateInfo::builder()
            .image(depth_image)
            .view_type(vk::ImageViewType::TYPE_2D)
            .format(vk::Format::D32_SFLOAT)
            .subresource_range(*subresource_range);
        let depth_imageview =
            unsafe { logical_device.create_image_view(&imageview_create_info, None) }?;

        let mut image_available = vec![];
        let mut rendering_finished = vec![];
        let mut may_begin_drawing = vec![];
        let semaphoreinfo = vk::SemaphoreCreateInfo::builder();
        let fenceinfo = vk::FenceCreateInfo::builder().flags(vk::FenceCreateFlags::SIGNALED);
        for _ in 0..amount_of_images {
            let semaphore_available =
                unsafe { logical_device.create_semaphore(&semaphoreinfo, None) }?;
            let semaphore_finished =
                unsafe { logical_device.create_semaphore(&semaphoreinfo, None) }?;
            image_available.push(semaphore_available);
            rendering_finished.push(semaphore_finished);
            let fence = unsafe { logical_device.create_fence(&fenceinfo, None) }?;
            may_begin_drawing.push(fence);
        }
        Ok(SwapchainDongXi {
            swapchain_loader,
            swapchain,
            images: swapchain_images,
            imageviews: swapchain_imageviews,
            depth_image,
            depth_image_allocation,
            depth_image_allocation_info,
            depth_imageview,
            framebuffers: vec![],
            surface_format,
            extent,
            amount_of_images,
            current_image: 0,
            image_available,
            rendering_finished,
            may_begin_drawing,
        })
    }

그리고 이 함수를 호출하기 전에 Allocator를 생성해야 하므로, Aetna::init()에서 순서를 일부 조정해야 합니다.

또한 SwapchainDongXi::cleanup()에도 몇 줄을 더 추가해야 합니다. 우리가 처리해야 할 새로운 ImageView와 Image가 있기 때문입니다:

    unsafe fn cleanup(&mut self, logical_device: &ash::Device, allocator: &vk_mem::Allocator) {
        logical_device.destroy_image_view(self.depth_imageview, None);
        allocator.destroy_image(self.depth_image, &self.depth_image_allocation);
        for fence in &self.may_begin_drawing {
            logical_device.destroy_fence(*fence, None);
        }
        for semaphore in &self.image_available {
            logical_device.destroy_semaphore(*semaphore, None);
        }
        for semaphore in &self.rendering_finished {
            logical_device.destroy_semaphore(*semaphore, None);
        }
        for fb in &self.framebuffers {
            logical_device.destroy_framebuffer(*fb, None);
        }
        for iv in &self.imageviews {
            logical_device.destroy_image_view(*iv, None);
        }
        self.swapchain_loader
            .destroy_swapchain(self.swapchain, None)
    }

좋습니다. 뎁스 이미지를 만들었습니다. 이제 사용할 시간입니다. 이것을 FrameBuffer에 포함시켜야 합니다.

    fn create_framebuffers(
        &mut self,
        logical_device: &ash::Device,
        renderpass: vk::RenderPass,
    ) -> Result<(), vk::Result> {
        for iv in &self.imageviews {
            let iview = [*iv, self.depth_imageview];
            let framebuffer_info = vk::FramebufferCreateInfo::builder()
                .render_pass(renderpass)
                .attachments(&iview)
                .width(self.extent.width)
                .height(self.extent.height)
                .layers(1);
            let fb = unsafe { logical_device.create_framebuffer(&framebuffer_info, None) }?;
            self.framebuffers.push(fb);
        }
        Ok(())
    }

한 줄이 변경되었습니다. 바로 우리가 사용하는 이미지 뷰입니다: let iview = [*iv, self.depth_imageview];.

다음과 같은 메시지를 받게 됩니다:

[Debug][error][validation] "vkCreateFramebuffer(): VkFramebufferCreateInfo attachmentCount of 2 does not match attachmentCount of 1 of renderPass (0x16) being used to create Frame
buffer. The Vulkan spec states: attachmentCount must be equal to the attachment count specified in renderPass (https://www.khronos.org/registry/vulkan/specs/1.1-extensions/html/vk
spec.html#VUID-VkFramebufferCreateInfo-attachmentCount-00876)"

우리는 렌더 패스에서 프레임버퍼를 사용하는데, 당연히 렌더 패스에 다른 첨부 파일(attachment)을 사용하려 한다는 사실을 알려줘야 합니다.

fn init_renderpass(
    logical_device: &ash::Device,
    format: vk::Format,
) -> Result<vk::RenderPass, vk::Result> {
    let attachments = [
        vk::AttachmentDescription::builder()
            .format(format)
            .load_op(vk::AttachmentLoadOp::CLEAR)
            .store_op(vk::AttachmentStoreOp::STORE)
            .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
            .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
            .initial_layout(vk::ImageLayout::UNDEFINED)
            .final_layout(vk::ImageLayout::PRESENT_SRC_KHR)
            .samples(vk::SampleCountFlags::TYPE_1)
            .build(),
        vk::AttachmentDescription::builder()
            .format(vk::Format::D32_SFLOAT)
            .load_op(vk::AttachmentLoadOp::CLEAR)
            .store_op(vk::AttachmentStoreOp::DONT_CARE)
            .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
            .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
            .initial_layout(vk::ImageLayout::UNDEFINED)
            .final_layout(vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL)
            .samples(vk::SampleCountFlags::TYPE_1)
            .build(),
    ];
    let color_attachment_references = [vk::AttachmentReference {
        attachment: 0,
        layout: vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
    }];
    let depth_attachment_reference = vk::AttachmentReference {
        attachment: 1,
        layout: vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
    };
    let subpasses = [vk::SubpassDescription::builder()
        .color_attachments(&color_attachment_references)
        .depth_stencil_attachment(&depth_attachment_reference)
        .pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS)
        .build()];
    let subpass_dependencies = [vk::SubpassDependency::builder()
        .src_subpass(vk::SUBPASS_EXTERNAL)
        .src_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)
        .dst_subpass(0)
        .dst_stage_mask(
            vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
        )
        .dst_access_mask(
            vk::AccessFlags::COLOR_ATTACHMENT_READ | vk::AccessFlags::COLOR_ATTACHMENT_WRITE
        )
        .build()];
    let renderpass_info = vk::RenderPassCreateInfo::builder()
        .attachments(&attachments)
        .subpasses(&subpasses)
        .dependencies(&subpass_dependencies);
    let renderpass = unsafe { logical_device.create_render_pass(&renderpass_info, None)? };
    Ok(renderpass)
}

무엇이 바뀌었을까요?

첫째, 약간의 정리를 했습니다: 불필요한 인자를 제거했습니다.

둘째, 두 번째 첨부 파일, 즉 우리의 뎁스 첨부 파일에 대한 설명이 추가되었습니다. 포맷은 이전과 같고, 로드 시 CLEAR, 저장 작업을 포함한 다른 작업에는 DONT_CARE를 사용합니다. (이것은 색상 첨부 파일과는 다릅니다. 뎁스 첨부 파일은 렌더링 중에만 사용되고, 이후에는 화면에 표시되는 등의 용도로 사용되지 않으므로 유지할 필요가 없습니다.) final_layout은 전용 뎁스 첨부 파일 형식이어야 합니다. (덧붙이자면, “depth”와 함께 자주 언급되는 “stencil”은 픽셀을 그리지 말아야 할지를 결정하는 또 다른 검사이지만, 우리는 사용하지 않습니다.)

셋째, 뎁스 첨부 파일에 대한 첨부 파일 참조(attachment reference)도 있습니다. 여기서도 레이아웃을 지정하고, 다음으로 사용 가능한 첨부 파일 번호를 사용합니다. 이 첨부 파일 참조는 서브패스(subpass)에 추가되어야 합니다. (색상 첨부 파일과의 작은 차이점: 이번에는 배열이 아니라 하나뿐입니다. 한 번에 하나의 뎁스 버퍼만 가질 수 있습니다.)

렌더패스: 완료. 다음 문제:

[Debug][error][validation] "Invalid Pipeline CreateInfo State: pDepthStencilState is NULL when rasterization is enabled and subpass uses a depth/stencil attachment. The Vulkan spec states: If the rasterizerDiscardEnable member of pRasterizationState is VK_FALSE, and subpass uses a depth/stencil attachment, pDepthStencilState must be a valid pointer to a valid VkPipelineDepthStencilStateCreateInfo structure (https://www.khronos.org/registry/vulkan/specs/1.1-extensions/html/vkspec.html#VUID-VkGraphicsPipelineCreateInfo-rasterizerDiscardEnable-00752)"

우리는 파이프라인에게 뎁스 정보로 무엇을 해야 할지 알려주지 않았습니다. 이전에는 뎁스 관련 사항이 없었으므로 그럴 필요가 없었죠. 이제는 있으니 DepthStencilState가 필요합니다.

따라서 Pipeline::init()pipeline_info에 한 줄이 추가됩니다:

        let pipeline_info = vk::GraphicsPipelineCreateInfo::builder()
            .stages(&shader_stages)
            .vertex_input_state(&vertex_input_info)
            .input_assembly_state(&input_assembly_info)
            .viewport_state(&viewport_info)
            .rasterization_state(&rasterizer_info)
            .multisample_state(&multisampler_info)
            .depth_stencil_state(&depth_stencil_info)
            .color_blend_state(&colourblend_info)
            .layout(pipelinelayout)
            .render_pass(*renderpass)
            .subpass(0);

“depth”가 들어간 줄이 새로 추가되었습니다. 물론, 이것만으로는 많은 것을 알 수 없습니다.

        let depth_stencil_info = vk::PipelineDepthStencilStateCreateInfo::builder()
            .depth_test_enable(true)
            .depth_write_enable(true)
            .depth_compare_op(vk::CompareOp::LESS_OR_EQUAL);

여기서 우리는 뎁스 테스트를 활성화하고(이 모든 작업을 하는 이유입니다), 새로운 프래그먼트의 깊이(z-값)가 같은 위치에 이미 있는 값보다 작거나 같을 경우에만 그려야 한다고 지정합니다. 또한 그 경우 뎁스 값을 업데이트하기로 결정합니다(depth_write_enable — 새로운 프래그먼트를 그릴지 여부를 확인하고 저장된 뎁스 값을 변경하지 않는 것도 가능합니다).

새로운 메시지를 받게 됩니다:

[Debug][error][validation] "In vkCmdBeginRenderPass() the VkRenderPassBeginInfo struct has a clearValueCount of 1 but there must be at least 2 entries in pClearValues array to account for the highest index attachment in renderPass 0x16 that uses VK_ATTACHMENT_LOAD_OP_CLEAR is 2. Note that the pClearValues array is indexed by attachment number so even if some pClearValues entries between 0 and 1 correspond to attachments that aren\'t cleared they will be ignored. The Vulkan spec states: clearValueCount must be greater than the largest attachment index in renderPass that specifies a loadOp (or stencilLoadOp, if the attachment has a depth/stencil format) of VK_ATTACHMENT_LOAD_OP_CLEAR (https://www.khronos.org/registry/vulkan/specs/1.1-extensions/html/vkspec.html#VUID-VkRenderPassBeginInfo-clearValueCount-00902)"

이것은 어느 정도 자명합니다: 렌더 패스에 두 번째 첨부 파일이 있고, 이를 초기화(clear)하겠다고 이미 말했지만, 이 두 번째 첨부 파일을 초기화할 때 어떤 값을 사용할지는 아직 프로그램에 알려주지 않았습니다.

fill_commandbuffers()renderpass_begininfo에는 일부 clearvalues가 포함되어 있으며, 이것을 조정해야 합니다.

        let clearvalues = [
            vk::ClearValue {
                color: vk::ClearColorValue {
                    float32: [0.0, 0.0, 0.08, 1.0],
                },
            },
            vk::ClearValue {
                depth_stencil: vk::ClearDepthStencilValue {
                    depth: 1.0,
                    stencil: 0,
                },
            },
        ];

두 번째 항목이 새로 추가되었고, 뎁스를 가능한 가장 높은 값인 1.0으로 설정하고, 스텐실은 0으로 초기화합니다(여전히 이 구성요소는 신경 쓰지 않습니다).

이제 노란색 상자는 항상 파란색 상자 앞에 그려집니다. 이것이 목표였습니다.

계속하기