Link

엔진이 μ •μƒμ μœΌλ‘œ μž‘λ™ν•˜κΈ΄ ν•˜μ§€λ§Œ, μ—¬μ „νžˆ λΉ„νš¨μœ¨μ μΈ 뢀뢄이 μ‘΄μž¬ν•©λ‹ˆλ‹€. 이제 μ΄λŸ¬ν•œ 뢀뢄듀을 κ°œμ„ ν•΄ λ³΄κ² μŠ΅λ‹ˆλ‹€.

타이머 UI

μ„±λŠ₯ μ΅œμ ν™”λ₯Ό μ‹œμž‘ν•˜κΈ° 전에, 각 μž‘μ—…μ΄ μ–Όλ§ˆλ‚˜ λΉ λ₯΄κ²Œ μˆ˜ν–‰λ˜λŠ”μ§€ μΈ‘μ •ν•  수 μžˆλŠ” μˆ˜λ‹¨μ΄ ν•„μš”ν•©λ‹ˆλ‹€. 이λ₯Ό μœ„ν•΄ std::chrono와 ImGuiλ₯Ό ν™œμš©ν•˜μ—¬ κ°„λ‹¨ν•œ λ²€μΉ˜λ§ˆν‚Ή 타이머 UIλ₯Ό μ„€μ •ν•˜κ² μŠ΅λ‹ˆλ‹€. μ›ν•œλ‹€λ©΄ Trancy와 같은 도ꡬλ₯Ό μ‚¬μš©ν•˜λŠ” 것도 κ°€λŠ₯ν•˜μ§€λ§Œ, μ—¬κΈ°μ„œλŠ” 기본적인 UI 기반 타이머λ₯Ό κ΅¬ν˜„ν•˜λŠ” λ°©μ‹μœΌλ‘œ μ§„ν–‰ν•˜κ² μŠ΅λ‹ˆλ‹€. GPUμΈ‘ μ„±λŠ₯ ν”„λ‘œνŒŒμΌλ§μ€ νŒŒμ΄ν”„λΌμΈ 쿼리 λ“± λ³΅μž‘ν•œ 섀정이 ν•„μš”ν•˜λ―€λ‘œ μ΄λ²ˆμ—λŠ” 닀루지 μ•ŠμŠ΅λ‹ˆλ‹€. μš°λ¦¬κ°€ ν•„μš”ν•œ μˆ˜μ€€μ—μ„œλŠ” NSight와 같은 GPU ν”„λ‘œνŒŒμΌλ§ 도ꡬλ₯Ό μ‚¬μš©ν•˜λŠ” 편이 μ’‹μŠ΅λ‹ˆλ‹€.

λ¨Όμ € μ‹œκ°„ κ΄€λ ¨ 정보λ₯Ό μ €μž₯ν•  ꡬ쑰체λ₯Ό vk_engine.h에 μΆ”κ°€ν•΄λ΄…μ‹œλ‹€.

struct EngineStats {
    float frametime;
    int triangle_count;
    int drawcall_count;
    float scene_update_time;
    float mesh_draw_time;
};

VulkanEngine ν΄λž˜μŠ€μ— EngineStats stats; 멀버λ₯Ό μΆ”κ°€ν•©λ‹ˆλ‹€.

ν”„λ ˆμž„ μ‹œκ°„μ€ 전체적인 μ‹œκ°„μ„ μΈ‘μ •ν•˜λŠ” 데 μ‚¬μš©λ˜λ©°, VSyncλ₯Ό μ‚¬μš©ν•˜κ³  있기 λ•Œλ¬Έμ— λŒ€λΆ€λΆ„ λͺ¨λ‹ˆν„° μ£Όμ‚¬μœ¨μ— 맞좰져 μžˆμ„ κ²ƒμž…λ‹ˆλ‹€. ν•˜μ§€λ§Œ κ·Έ μ™Έμ˜ 값듀은 μ„±λŠ₯ 츑정에 더 μœ μš©ν•˜κ²Œ 쓰일 수 μžˆμŠ΅λ‹ˆλ‹€.

그럼 ν”„λ ˆμž„ μ‹œκ°„μ„ κ³„μ‚°ν•΄λ΄…μ‹œλ‹€.

μ—”μ§„μ˜ 메인 루프인 run()에 μ•„λž˜ μ½”λ“œλ₯Ό μΆ”κ°€ν•©λ‹ˆλ‹€.

// main loop
while (!bQuit) {
    //begin clock
    auto start = std::chrono::system_clock::now();

    //everything else

    //get clock again, compare with start clock
    auto end = std::chrono::system_clock::now();
     
     //convert to microseconds (integer), and then come back to miliseconds
    auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
    stats.frametime = elapsed.count() / 1000.f;
}

auto start = std::chrono::system_clock::now();λ₯Ό μ‚¬μš©ν•˜λ©΄ ν˜„μž¬ μ‹œκ°„μ„ 높은 μ •λ°€λ„λ‘œ μΈ‘μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 이 값을 이후 λ‹€μ‹œ 호좜된 μ‹œμ μ˜ μ‹œκ°„κ³Ό λΉ„κ΅ν•˜λ©΄, νŠΉμ • ꡬ간이 μ‹€ν–‰λ˜λŠ” 데 μ–Όλ§ˆλ‚˜ κ±Έλ ΈλŠ”μ§€λ₯Ό μ•Œ 수 μžˆμŠ΅λ‹ˆλ‹€. 이λ₯Ό λ°€λ¦¬μ΄ˆ(ms) λ‹¨μœ„μ˜ μ‹œκ°„μœΌλ‘œ λ³€ν™˜ν•˜λ €λ©΄, λ¨Όμ € 마이크둜초둜 μΊμŠ€νŒ…ν•œ λ’€ 1000.fλ₯Ό κ³±ν•΄μ•Ό ν•©λ‹ˆλ‹€. μ΄λ ‡κ²Œ ν•˜λ©΄ μ†Œμˆ˜μ  μ…‹μ§Έ μžλ¦¬κΉŒμ§€ ν‘œν˜„λ˜λŠ” 값이 λ©λ‹ˆλ‹€.

draw_geometry ν•¨μˆ˜μ—μ„œ ν•΄λ‹Ή μ½”λ“œλ₯Ό μΆ”κ°€ν•˜κ³  λ™μ‹œμ— λ“œλ‘œμš°μ½œ νšŸμˆ˜μ™€ μ‚Όκ°ν˜• 수λ₯Ό κ³„μ‚°ν•˜λŠ” μ½”λ“œλ„ λ„£μ–΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

void VulkanEngine::draw_geometry(VkCommandBuffer cmd)
{
    //reset counters
    stats.drawcall_count = 0;
    stats.triangle_count = 0;
    //begin clock
    auto start = std::chrono::system_clock::now();

    /* code */

    auto draw = [&](const RenderObject& r) {

        /* drawing code */

        vkCmdDrawIndexed(cmd, draw.indexCount, 1, draw.firstIndex, 0, 0);

        //add counters for triangles and draws
        stats.drawcall_count++;
        stats.triangle_count += draw.indexCount / 3;   
    }

    /* code */


    auto end = std::chrono::system_clock::now();

    //convert to microseconds (integer), and then come back to miliseconds
    auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
    stats.mesh_draw_time = elapsed.count() / 1000.f;
}

μ‹œμž‘ν•  λ•Œ μΉ΄μš΄ν„°λ₯Ό 0으둜 μ΄ˆκΈ°ν™”ν•©λ‹ˆλ‹€. 그런 λ‹€μŒ draw λžŒλ‹€μ—μ„œ 그리기 둜직 이후에 λ“œλ‘œμš°μ½œ νšŸμˆ˜μ™€ μ‚Όκ°ν˜• 개수λ₯Ό λˆ„μ ν•©λ‹ˆλ‹€. ν•¨μˆ˜μ˜ λ§ˆμ§€λ§‰μ—λŠ” μ΅œμ’… μ‹œκ°„μ„ μΈ‘μ •ν•˜μ—¬ stats ꡬ쑰체에 μ €μž₯ν•©λ‹ˆλ‹€.

λ™μΌν•œ μž‘μ—…μ„ update_scene()ν•¨μˆ˜μ—μ„œλ„ μˆ˜ν–‰ν•˜λ˜, μ‹œμž‘/μ’…λ£Œ μ‹œκ°„μ„ μΈ‘μ •ν•΄ scene_update_time에 μ €μž₯ν•©λ‹ˆλ‹€.

이제 ImGuiλ₯Ό μ‚¬μš©ν•΄ 정보λ₯Ό 화면에 ν‘œμ‹œν•΄μ•Ό ν•©λ‹ˆλ‹€.

run() ν•¨μˆ˜μ—μ„œ ImGui::NewFrame()κ³Ό ImGui::Render() 사이에 μ•„λž˜ μ½”λ“œλ₯Ό μΆ”κ°€ν•˜μ—¬ μƒˆλ‘œμš΄ ImGui 창을 κ·Έλ¦½λ‹ˆλ‹€.

        ImGui::Begin("Stats");

        ImGui::Text("frametime %f ms", stats.frametime);
        ImGui::Text("draw time %f ms", stats.mesh_draw_time);
        ImGui::Text("update time %f ms", stats.scene_update_time);
        ImGui::Text("triangles %i", stats.triangle_count);
        ImGui::Text("draws %i", stats.drawcall_count);
        ImGui::End();

μ—¬κΈ°μ„œ 엔진을 μ‹€ν–‰ν•œλ‹€λ©΄ 각 정보λ₯Ό 확인할 수 μžˆμŠ΅λ‹ˆλ‹€. ν˜„μž¬λŠ” 검증 λ ˆμ΄μ–΄κ°€ ν™œμ„±ν™”λ˜μ–΄ 있고, 디버그 λͺ¨λ“œκ°€ μΌœμ Έμžˆμ„ κ°€λŠ₯성이 λ†’μŠ΅λ‹ˆλ‹€. 컴파일러 μ„€μ •μ—μ„œ 릴리즈 λͺ¨λ“œλ₯Ό ν™œμ„±ν™”ν•˜κ³ , constexpr bool bUseValidationLayers = true 값을 false둜 λ³€κ²½ν•΄ 검증 λ ˆμ΄μ–΄λ₯Ό λΉ„ν™œμ„±ν™”ν•˜μ„Έμš”. 라이젠 5950x CPU κΈ°μ€€μœΌλ‘œ 검증 λ ˆμ΄μ–΄λ₯Ό λΉ„ν™œμ„±ν™”ν•˜λ©΄ draw_time이 μ•½ 6.5msμ—μ„œ 0.3ms둜 μ€„μ–΄λ“­λ‹ˆλ‹€. 씬이 μ˜¬λ°”λ₯΄κ²Œ λ Œλ”λ§λ˜κ³  μžˆλ‹€λ©΄ λ“œλ‘œμš°μ½œ νšŸμˆ˜λŠ” 1700으둜 ν‘œμ‹œλ  κ²ƒμž…λ‹ˆλ‹€.

μ •λ ¬ ν›„ 그리기

ν˜„μž¬ μš°λ¦¬λŠ” λ§€ λ“œλ‘œμš°μ½œλ§ˆλ‹€ νŒŒμ΄ν”„λΌμΈ 등을 λ°˜λ³΅ν•΄μ„œ λ°”μΈλ”©ν•˜κ³  있기 λ•Œλ¬Έμ—, ν•„μš” μ΄μƒμœΌλ‘œ λ§Žμ€ Vulkan ν˜ΈμΆœμ„ μˆ˜ν–‰ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. λ°”μΈλ”©λœ μƒνƒœλ₯Ό μΆ”μ ν•˜κ³ , μƒνƒœκ°€ μ‹€μ œλ‘œ 변경될 λ•Œμ—λ§Œ ν•΄λ‹Ή Vulkan ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν•˜λ„λ‘ ν•΄μ•Ό ν•©λ‹ˆλ‹€.

이전 κΈ€μ—μ„œ μ†Œκ°œν–ˆλ˜ draw() λžŒλ‹€λ₯Ό μˆ˜μ •ν•˜μ—¬ μƒνƒœ 좔적 κΈ°λŠ₯을 μΆ”κ°€ν•  κ²ƒμž…λ‹ˆλ‹€. νŒŒλΌλ―Έν„°κ°€ λ³€κ²½λœ κ²½μš°μ—λ§Œ Vulkan ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν•˜λ„λ‘ κ΅¬ν˜„ν•˜κ² μŠ΅λ‹ˆλ‹€.

//defined outside of the draw function, this is the state we will try to skip
 MaterialPipeline* lastPipeline = nullptr;
 MaterialInstance* lastMaterial = nullptr;
 VkBuffer lastIndexBuffer = VK_NULL_HANDLE;

 auto draw = [&](const RenderObject& r) {
     if (r.material != lastMaterial) {
         lastMaterial = r.material;
         //rebind pipeline and descriptors if the material changed
         if (r.material->pipeline != lastPipeline) {

             lastPipeline = r.material->pipeline;
             vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, r.material->pipeline->pipeline);
             vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,r.material->pipeline->layout, 0, 1,
                 &globalDescriptor, 0, nullptr);

            VkViewport viewport = {};
            viewport.x = 0;
            viewport.y = 0;
            viewport.width = (float)_windowExtent.width;
            viewport.height = (float)_windowExtent.height;
            viewport.minDepth = 0.f;
            viewport.maxDepth = 1.f;

            vkCmdSetViewport(cmd, 0, 1, &viewport);

            VkRect2D scissor = {};
            scissor.offset.x = 0;
            scissor.offset.y = 0;
            scissor.extent.width = _windowExtent.width;
            scissor.extent.height = _windowExtent.height;

            vkCmdSetScissor(cmd, 0, 1, &scissor);
         }

         vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, r.material->pipeline->layout, 1, 1,
             &r.material->materialSet, 0, nullptr);
     }
    //rebind index buffer if needed
     if (r.indexBuffer != lastIndexBuffer) {
         lastIndexBuffer = r.indexBuffer;
         vkCmdBindIndexBuffer(cmd, r.indexBuffer, 0, VK_INDEX_TYPE_UINT32);
     }
     // calculate final mesh matrix
     GPUDrawPushConstants push_constants;
     push_constants.worldMatrix = r.transform;
     push_constants.vertexBuffer = r.vertexBufferAddress;

     vkCmdPushConstants(cmd, r.material->pipeline->layout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(GPUDrawPushConstants), &push_constants);

     vkCmdDrawIndexed(cmd, r.indexCount, 1, r.firstIndex, 0, 0);
    //stats
    stats.drawcall_count++;
    stats.triangle_count += r.indexCount / 3;
 };

λ§ˆμ§€λ§‰μœΌλ‘œ μ‚¬μš©ν•œ νŒŒμ΄ν”„λΌμΈ, λ¨Έν…Œλ¦¬μ–Ό, 인덱슀 버퍼λ₯Ό μ €μž₯ν•©λ‹ˆλ‹€. λ¨Όμ € νŒŒμ΄ν”„λΌμΈμ΄ λ°”λ€Œμ—ˆλŠ”μ§€ ν™•μΈν•˜κ³ , λ°”λ€Œμ—ˆλ‹€λ©΄ νŒŒμ΄ν”„λΌμΈμ„ λ‹€μ‹œ λ°”μΈλ”©ν•˜μ—¬ μ „μ—­ λ””μŠ€ν¬λ¦½ν„° 셋도 ν•¨κ»˜ λ°”μΈλ”©ν•©λ‹ˆλ‹€. μ΄λ•Œ SetViewport와 SetScissor λͺ…령도 λ‹€μ‹œ ν˜ΈμΆœν•΄μ•Ό ν•©λ‹ˆλ‹€.

κ·Έ λ‹€μŒ, λ¨Έν…Œλ¦¬μ–Όμ΄ λ³€κ²½λ˜λ©΄ λ¨Έν…Œλ¦¬μ–Ό νŒŒλΌλ―Έν„°μ™€ ν…μŠ€μ³λ₯Ό μœ„ν•œ λ””μŠ€ν¬λ¦½ν„° 셋을 λ°”μΈλ”©ν•©λ‹ˆλ‹€. λ§ˆμ§€λ§‰μœΌλ‘œ 인덱슀 버퍼가 λ³€κ²½λ˜λ©΄ ν•΄λ‹Ή 인덱슀 버퍼도 λ‹€μ‹œ λ°”μΈλ”©ν•©λ‹ˆλ‹€.

이제 단 2개의 νŒŒμ΄ν”„λΌμΈλ§Œ μ‚¬μš©λ˜λ―€λ‘œ vkCmdBindPipeline 호좜이 λŒ€ν­ 쀄어듀어 μ„±λŠ₯ ν–₯상을 κΈ°λŒ€ν•  수 μžˆμŠ΅λ‹ˆλ‹€. ν•˜μ§€λ§Œ μ—¬κΈ°μ„œ 더 μ΅œμ ν™”ν•΄λ³΄κ² μŠ΅λ‹ˆλ‹€. 라이젠 5950x 컴퓨터 κΈ°μ€€μœΌλ‘œ draw_geometry의 μ‹€ν–‰ μ‹œκ°„μ΄ 절반으둜 λ‹¨μΆ•λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

호좜 수λ₯Ό μ΅œμ†Œν™”ν•˜κΈ° μœ„ν•΄ λ Œλ”λ§ν•  객체듀을 μ΄λŸ¬ν•œ νŒŒλΌλ―Έν„° κΈ°μ€€μœΌλ‘œ μ •λ ¬ν•΄μ•Ό ν•©λ‹ˆλ‹€. 이 정렬은 뢈투λͺ… κ°μ²΄μ—λ§Œ μˆ˜ν–‰ν•˜κ² μŠ΅λ‹ˆλ‹€. 투λͺ…ν•œ 객체듀은 깊이 정렬이 ν•„μš”ν•˜μ§€λ§Œ, 아직은 ν•΄λ‹Ή 정보λ₯Ό κ°€μ§€κ³  μžˆμ§€ μ•ŠκΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€.

정렬을 κ΅¬ν˜„ν•  λ•Œ, 객체 μžμ²΄κ°€ 크기 λ•Œλ¬Έμ— draw λ°°μ—΄ κ·Έ 자체λ₯Ό μ •λ ¬ν•˜μ§€λŠ” μ•Šμ„ κ²ƒμž…λ‹ˆλ‹€. λŒ€μ‹ , ν•΄λ‹Ή 배열을 μ°Έμ‘°ν•˜λŠ” 인덱슀 배열을 μ •λ ¬ν•˜κ² μŠ΅λ‹ˆλ‹€. μ΄λŠ” λŒ€ν˜• μ—”μ§„μ—μ„œλ„ 일반적으둜 μ‚¬μš©λ˜λŠ” κΈ°λ²•μž…λ‹ˆλ‹€.

draw_geometry() ν•¨μˆ˜μ˜ μ‹œμž‘ 뢀뢄에 λ‹€μŒ μ½”λ“œλ₯Ό μΆ”κ°€ν•˜μ„Έμš”.

std::vector<uint32_t> opaque_draws;
opaque_draws.reserve(mainDrawContext.OpaqueSurfaces.size());

for (uint32_t i = 0; i < mainDrawContext.OpaqueSurfaces.size(); i++) {
    opaque_draws.push_back(i);
}

// sort the opaque surfaces by material and mesh
std::sort(opaque_draws.begin(), opaque_draws.end(), [&](const auto& iA, const auto& iB) {
    const RenderObject& A = mainDrawContext.OpaqueSurfaces[iA];
    const RenderObject& B = mainDrawContext.OpaqueSurfaces[iB];
    if (A.material == B.material) {
        return A.indexBuffer < B.indexBuffer;
    }
    else {
        return A.material < B.material;
    }
});

std::algorithmsμ—λŠ” opaque_draws 벑터λ₯Ό μ •λ ¬ν•˜λŠ” 데 μœ μš©ν•œ μ •λ ¬ ν•¨μˆ˜κ°€ μžˆμŠ΅λ‹ˆλ‹€. 여기에 < μ—°μ‚°μžλ₯Ό μ •μ˜ν•œ λžŒλ‹€λ₯Ό μ „λ‹¬ν•˜λ©΄ 효율적으둜 μ •λ ¬ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

λ¨Όμ € draw λ°°μ—΄μ˜ 인덱슀λ₯Ό κΈ°μ€€μœΌλ‘œ λ¨Έν…Œλ¦¬μ–Όμ΄ 같은지 ν™•μΈν•˜κ³ , κ°™λ‹€λ©΄ 인덱슀 버퍼λ₯Ό κΈ°μ€€μœΌλ‘œ μ •λ ¬ν•©λ‹ˆλ‹€. λ§Œμ•½ λ¨Έν…Œλ¦¬μ–Όμ΄ λ‹€λ₯΄λ‹€λ©΄ λ¨Έν…Œλ¦¬μ–Ό 포인터 κ·Έ 자체λ₯Ό λΉ„κ΅ν•©λ‹ˆλ‹€. 이λ₯Ό μˆ˜ν–‰ν•˜λŠ” 또 λ‹€λ₯Έ 방법은 μ •λ ¬ ν‚€λ₯Ό κ³„μ‚°ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€. 이 경우 opaque_drawsλŠ” 20λΉ„νŠΈμ˜ μΈλ±μŠ€μ™€ 44λΉ„νŠΈμ˜ μ •λ ¬ ν‚€(λ˜λŠ” ν•΄μ‹œ)둜 ꡬ성될 수 μžˆμŠ΅λ‹ˆλ‹€. 이 방식은 보닀 λΉ λ₯Έ μ •λ ¬ μ•Œκ³ λ¦¬μ¦˜μ„ μ‚¬μš©ν•  수 있기 λ•Œλ¬Έμ— μ„±λŠ₯상 더 μœ λ¦¬ν•©λ‹ˆλ‹€.

이제 μ •λ ¬λœ 배열을 기반으둜 λ“œλ‘œμš°λ₯Ό μˆ˜ν–‰ν•©λ‹ˆλ‹€. 기쑴의 뢈투λͺ… 객체 λ“œλ‘œμš° λ°˜λ³΅λ¬Έμ„ μ•„λž˜ μ½”λ“œλ‘œ λŒ€μ²΄ν•©μ‹œλ‹€.

for (auto& r : opaque_draws) {
    draw(mainDrawContext.OpaqueSurfaces[r]);
}

μ΄λ ‡κ²Œ ν•˜λ©΄ λ Œλ”λŸ¬κ°€ λ””μŠ€ν¬λ¦½ν„° μ…‹ 바인딩 수λ₯Ό μ΅œμ†Œν™”ν•  수 μžˆμŠ΅λ‹ˆλ‹€. λ¨Έν…Œλ¦¬μ–Ό λ‹¨μœ„λ‘œ μ²˜λ¦¬ν•˜κΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€. 인덱슀 버퍼 바인딩은 μ—¬μ „νžˆ μ²˜λ¦¬ν•΄μ•Ό ν•˜μ§€λ§Œ, μ΄λŠ” 비ꡐ적 λΉ λ₯΄κ²Œ μ „ν™˜λ©λ‹ˆλ‹€.

μ΄λ ‡κ²Œ ν•˜λ©΄ μ—”μ§„μ˜ μ„±λŠ₯이 μ•½κ°„ ν–₯상될 수 μžˆμŠ΅λ‹ˆλ‹€. ν•˜μ§€λ§Œ ν•˜λ‚˜μ˜ μ”¬λ§Œ λ Œλ”λ§ν•˜λŠ” 경우 μ •λ ¬ λΉ„μš© λ•Œλ¬Έμ— μ„±λŠ₯ ν–₯상 νš¨κ³Όκ°€ 거의 없을 수 μžˆμŠ΅λ‹ˆλ‹€. ν˜„μž¬ μš°λ¦¬λŠ” λΉ„νš¨μœ¨μ μΈ 단일 μ“°λ ˆλ“œ 정렬을 μˆ˜ν–‰ν•˜κ³  있기 λ•Œλ¬Έμ—, μ •λ ¬ λΉ„μš©μ΄ Vulkan 호좜 κ°μ†Œλ‘œ μ–»λŠ” μ„±λŠ₯ ν–₯상을 상쇄할 μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€. λ°˜λ“œμ‹œ 씬을 ν”„λ‘œνŒŒμΌλ§ν•˜μ—¬ 이 κΈ°λŠ₯을 ν™œμ„±ν™”ν• μ§€ μ—¬λΆ€λ₯Ό κ²°μ •ν•˜μ„Έμš”.

μ„±λŠ₯이 κ°œμ„ λ˜μ—ˆμ§€λ§Œ 아직 κ°œμ„ ν•  뢀뢄이 λ‚¨μ•„μžˆμŠ΅λ‹ˆλ‹€. ν˜„μž¬ μš°λ¦¬λŠ” 카메라 뒀에 μžˆλŠ” κ°μ²΄λ“€κΉŒμ§€ λ Œλ”λ§ν•˜κ³  μžˆμ–΄, CPU와 GPU λͺ¨λ‘μ—μ„œ λΆˆν•„μš”ν•˜κ²Œ λ§Žμ€ 객체λ₯Ό μ²˜λ¦¬ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. 이λ₯Ό ν”„λŸ¬μŠ€ν…€ 컬링을 톡해 κ°œμ„ ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

ν”„λŸ¬μŠ€ν…€ 컬링

ν˜„μž¬λŠ” 맡에 μžˆλŠ” λͺ¨λ“  객체λ₯Ό λ Œλ”λ§ν•˜κ³  μžˆμ§€λ§Œ, μ‹œμ•Ό 밖에 μžˆλŠ” κ²ƒλ“€κΉŒμ§€ 그릴 ν•„μš”λŠ” μ—†μŠ΅λ‹ˆλ‹€. μš°λ¦¬λŠ” 이미 그릴 객체의 λͺ©λ‘μ„ κ°€μ§€κ³  μžˆμœΌλ―€λ‘œ, 이λ₯Ό ν•„ν„°λ§ν•˜μ—¬ μ‹œμ•Ό μ•ˆμ— λ“€μ–΄μ˜€λŠ” 객체만 λ Œλ”λ§ν•˜κ³ , κ·Έλ ‡μ§€ μ•Šμ€ κ°μ²΄λŠ” κ±΄λ„ˆλ›°κ² μŠ΅λ‹ˆλ‹€. 필터링 λΉ„μš©μ΄ 객체λ₯Ό λ Œλ”λ§ν•˜λŠ” λΉ„μš©λ³΄λ‹€ 적기만 ν•˜λ‹€λ©΄, μ„±λŠ₯μƒμ˜ 이득을 얻을 수 μžˆμŠ΅λ‹ˆλ‹€.

ν”„λŸ¬μŠ€ν…€ 컬링을 μˆ˜ν–‰ν•˜λŠ” λ°©λ²•μ—λŠ” μ—¬λŸ¬ κ°€μ§€κ°€ μžˆμ§€λ§Œ, ν˜„μž¬ μš°λ¦¬κ°€ κ°€μ§„ 데이터와 μ•„ν‚€ν…μ²˜μ— μ ν•©ν•œ 방식은 OBB(Oriented Bounding Box)λ₯Ό μ‚¬μš©ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€. 각 GeoSurfaceλ§ˆλ‹€ λ°”μš΄λ”© λ°•μŠ€λ₯Ό κ³„μ‚°ν•˜κ³ , 그것이 μ‹œμ•Όμ— λ“€μ–΄μ˜€λŠ”μ§€ 확인할 κ²ƒμž…λ‹ˆλ‹€.

vk_loaders.h의 ꡬ쑰체에 λ°”μš΄λ”© 정보λ₯Ό μΆ”κ°€ν•˜μ„Έμš”.

struct Bounds {
    glm::vec3 origin;
    float sphereRadius;
    glm::vec3 extents;
};

struct GeoSurface {
    uint32_t startIndex;
    uint32_t count;
    Bounds bounds;
	std::shared_ptr<GLTFMaterial> material;
};

RenderObject에 Bounds ꡬ쑰체λ₯Ό μΆ”κ°€ν•©λ‹ˆλ‹€.

struct RenderObject {
    uint32_t indexCount;
    uint32_t firstIndex;
    VkBuffer indexBuffer;
    
    MaterialInstance* material;
    Bounds bounds;
    glm::mat4 transform;
    VkDeviceAddress vertexBufferAddress;
};

Bounds κ΅¬μ‘°μ²΄λŠ” 쀑심점, 크기, ꡬ의 λ°˜μ§€λ¦„μ„ ν¬ν•¨ν•©λ‹ˆλ‹€. ꡬ의 λ°˜μ§€λ¦„μ€ λ‹€λ₯Έ ν”„λŸ¬μŠ€ν…€ 컬링 μ•Œκ³ λ¦¬μ¦˜μ΄λ‚˜ κ·Έ μ™Έ λ‹€λ₯Έ μš©λ„λ‘œλ„ μ‚¬μš©λ  수 μžˆμŠ΅λ‹ˆλ‹€.

이λ₯Ό κ³„μ‚°ν•˜κΈ° μœ„ν•΄, ν•΄λ‹Ή λ‘œμ§μ„ loader μ½”λ“œμ— μΆ”κ°€ν•΄μ•Ό ν•©λ‹ˆλ‹€.

이 μ½”λ“œλŠ” λ©”μ‹œ 데이터λ₯Ό λΆˆλŸ¬μ˜€λŠ” 반볡문의 끝뢀뢄에 μžˆλŠ” loadGLTF ν•¨μˆ˜ 내뢀에 μž‘μ„±ν•  κ²ƒμž…λ‹ˆλ‹€.

//code that writes vertex buffers

//loop the vertices of this surface, find min/max bounds
glm::vec3 minpos = vertices[initial_vtx].position;
glm::vec3 maxpos = vertices[initial_vtx].position;
for (int i = initial_vtx; i < vertices.size(); i++) {
    minpos = glm::min(minpos, vertices[i].position);
    maxpos = glm::max(maxpos, vertices[i].position);
}
// calculate origin and extents from the min/max, use extent lenght for radius
newSurface.bounds.origin = (maxpos + minpos) / 2.f;
newSurface.bounds.extents = (maxpos - minpos) / 2.f;
newSurface.bounds.sphereRadius = glm::length(newSurface.bounds.extents);

newmesh->surfaces.push_back(newSurface);

MeshNode::Draw() ν•¨μˆ˜μ—μ„œ Boundsκ°€ RenderOBject둜 λ³΅μ‚¬λ˜λ„λ‘ ν•΄μ•Ό ν•©λ‹ˆλ‹€. μ΄λŠ” λ‹€μŒκ³Ό 같이 보일 κ²ƒμž…λ‹ˆλ‹€.

void MeshNode::Draw(const glm::mat4& topMatrix, DrawContext& ctx) {
    glm::mat4 nodeMatrix = topMatrix * worldTransform;

    for (auto& s : mesh->surfaces) {
        RenderObject def;
        def.indexCount = s.count;
        def.firstIndex = s.startIndex;
        def.indexBuffer = mesh->meshBuffers.indexBuffer.buffer;
        def.material = &s.material->data;
        def.bounds = s.bounds;
        def.transform = nodeMatrix;
        def.vertexBufferAddress = mesh->meshBuffers.vertexBufferAddress;

        if (s.material->data.passType == MaterialPass::Transparent) {
            ctx.TransparentSurfaces.push_back(def);
        } else {
            ctx.OpaqueSurfaces.push_back(def);
        }
    }

    // recurse down
    Node::Draw(topMatrix, ctx);
}

이제 GeoSurface에 Boundsκ°€ μ„€μ •λ˜μ—ˆμœΌλ―€λ‘œ, RenderObjectκ°€ μ‹œμ•Όμ— λ“€μ–΄μ˜€λŠ” μ§€ 확인해야 ν•©λ‹ˆλ‹€. μ•„λž˜ ν•¨μˆ˜λ₯Ό vk_engine.cpp에 μ „μ—­ ν•¨μˆ˜λ‘œ μΆ”κ°€ν•˜μ„Έμš”.

bool is_visible(const RenderObject& obj, const glm::mat4& viewproj) {
    std::array<glm::vec3, 8> corners {
        glm::vec3 { 1, 1, 1 },
        glm::vec3 { 1, 1, -1 },
        glm::vec3 { 1, -1, 1 },
        glm::vec3 { 1, -1, -1 },
        glm::vec3 { -1, 1, 1 },
        glm::vec3 { -1, 1, -1 },
        glm::vec3 { -1, -1, 1 },
        glm::vec3 { -1, -1, -1 },
    };

    glm::mat4 matrix = viewproj * obj.transform;

    glm::vec3 min = { 1.5, 1.5, 1.5 };
    glm::vec3 max = { -1.5, -1.5, -1.5 };

    for (int c = 0; c < 8; c++) {
        // project each corner into clip space
        glm::vec4 v = matrix * glm::vec4(obj.bounds.origin + (corners[c] * obj.bounds.extents), 1.f);

        // perspective correction
        v.x = v.x / v.w;
        v.y = v.y / v.w;
        v.z = v.z / v.w;

        min = glm::min(glm::vec3 { v.x, v.y, v.z }, min);
        max = glm::max(glm::vec3 { v.x, v.y, v.z }, max);
    }

    // check the clip space box is within the view
    if (min.z > 1.f || max.z < 0.f || min.x > 1.f || max.x < -1.f || min.y > 1.f || max.y < -1.f) {
        return false;
    } else {
        return true;
    }
}

μ΄λŠ” ν”„λŸ¬μŠ€ν…€ 컬링을 μˆ˜ν–‰ν•  수 μžˆλŠ” μ—¬λŸ¬ 방법 쀑 ν•˜λ‚˜μž…λ‹ˆλ‹€. 이 방식은 λ©”μ‹œ κ³΅κ°„μ˜ λ°”μš΄λ”© λ°•μŠ€ 8개의 꼭짓점을 객체 ν–‰λ ¬κ³Ό λ·° ν”„λ‘œμ μ…˜ 행렬을 μ‚¬μš©ν•΄ 슀크린 κ³΅κ°„μœΌλ‘œ λ³€ν™˜ν•˜μ—¬ μž‘λ™ν•©λ‹ˆλ‹€. λ³€ν™˜λœ κΌ­μ§“μ μœΌλ‘œλΆ€ν„° 슀크린 κ³΅κ°„μ˜ λ°”μš΄λ”© λ°•μŠ€λ₯Ό κ³„μ‚°ν•œ λ’€, ν•΄λ‹Ή λ°•μŠ€κ°€ 클립 곡간 λ·° μ•ˆμ— μžˆλŠ”μ§€ ν™•μΈν•©λ‹ˆλ‹€. μ΄λŸ¬ν•œ 경계 계산 방식은 λ‹€λ₯Έ 곡식듀에 λΉ„ν•΄ λ‹€μ†Œ 느린 편이며, μ‹€μ œλ‘œλŠ” 보이지 μ•ŠλŠ” 객체λ₯Ό λ³΄μ΄λŠ” κ²ƒμœΌλ‘œ νŒλ‹¨ν•˜λŠ” false positiveκ°€ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€. 각 λ°©μ‹λ§ˆλ‹€ νŠΈλ ˆμ΄λ“œμ˜€ν”„κ°€ μ‘΄μž¬ν•˜λ©°, 이 방식은 λ‹¨μˆœμ„±κ³Ό 정점 μ…°μ΄λ”μ—μ„œ μˆ˜ν–‰ν•˜λŠ” μž‘μ—…κ³Όμ˜ μœ μ‚¬μ„± λ•Œλ¬Έμ— μ„ νƒν•˜μ˜€μŠ΅λ‹ˆλ‹€.

이 ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•˜λ €λ©΄, opaque_draws 배열을 μ±„μš°λŠ” 루프λ₯Ό μˆ˜μ •ν•΄μ•Ό ν•©λ‹ˆλ‹€.

for (int i = 0; i < mainDrawContext.OpaqueSurfaces.size(); i++) {
	if (is_visible(mainDrawContext.OpaqueSurfaces[i], sceneData.viewproj)) {
		opaque_draws.push_back(i);
	}
}

이제 iλ₯Ό μΆ”κ°€ν•˜λŠ” λŒ€μ‹ , 객체가 μ‹œμ•Όμ— λ“€μ–΄μ˜€λŠ” μ§€ ν™•μΈν•©λ‹ˆλ‹€.

이제 λ Œλ”λŸ¬λŠ” μ‹œμ•Ό μ™ΈλΆ€μ˜ 객체듀을 κ±΄λ„ˆλ›Έ κ²ƒμž…λ‹ˆλ‹€. 화면에 λ³΄μ΄λŠ” κ²°κ³ΌλŠ” λ™μΌν•˜μ§€λ§Œ, λ“œλ‘œμš°μ½œ μˆ˜κ°€ 쀄어듀고 μ„±λŠ₯은 더 빨라질 κ²ƒμž…λ‹ˆλ‹€. λ§Œμ•½ μ‹œκ°μ  이상이 λ°œμƒν•œλ‹€λ©΄, GeoSurface의 λ°”μš΄λ”© λ°•μŠ€ 생성 κ³Όμ •κ³Ό is_visible ν•¨μˆ˜μ— μ˜€νƒ€κ°€ μ—†λŠ”μ§€ λ‹€μ‹œ ν™•μΈν•΄λ³΄μ„Έμš”

투λͺ…ν•œ 객체에 λŒ€ν•΄μ„œλ„ λ™μΌν•œ 컬링과 정렬을 μ μš©ν•˜λŠ” μ½”λ“œλŠ” κ±΄λ„ˆλ›°μ—ˆμŠ΅λ‹ˆλ‹€. 뢈투λͺ… 객체에 μ μš©ν•œ 방식과 λ™μΌν•˜λ―€λ‘œ 직접 κ΅¬ν˜„ν•΄λ³΄μ„Έμš”.

투λͺ…ν•œ 객체의 경우, 경계와 카메라 κ°„μ˜ 거리λ₯Ό κΈ°μ€€μœΌλ‘œ μ •λ ¬ν•˜λŠ” λ°©μ‹μœΌλ‘œ μ •λ ¬ μ½”λ“œλ₯Ό λ³€κ²½ν•΄μ•Ό 더 μ •ν™•ν•˜κ²Œ λ Œλ”λ§ν•  수 μžˆμŠ΅λ‹ˆλ‹€. λ‹€λ§Œ 깊이 κΈ°μ€€ 정렬은 νŒŒμ΄ν”„λΌμΈ κΈ°μ€€ μ •λ ¬κ³Ό ν˜Έν™˜λ˜μ§€ μ•ŠκΈ° λ•Œλ¬Έμ—, μ–΄λ–€ 방식이 μžμ‹ μ˜ 상황에 더 μ ν•©ν•œμ§€ κ²°μ •ν•΄μ•Ό ν•©λ‹ˆλ‹€.

카메라λ₯Ό νšŒμ „μ‹œν‚€λ©΄μ„œ μŠ€νƒ― μ°½μ—μ„œ λ“œλ‘œμš°μ½œ μˆ˜μ™€ μ‚Όκ°ν˜• κ°œμˆ˜κ°€ λ³€ν™”ν•˜λŠ” 것을 확인할 수 μžˆμ„ κ²ƒμž…λ‹ˆλ‹€. 특히 객체λ₯Ό 바라보지 μ•ŠλŠ” μƒν™©μ—μ„œλŠ” μ„±λŠ₯이 λˆˆμ— λ„κ²Œ ν–₯상될 κ²ƒμž…λ‹ˆλ‹€.

λ°‰λ§΅ μƒμ„±ν•˜κΈ°

ν…μŠ€μ³λ₯Ό 뢈러올 λ•Œ 밉맡을 μƒμ„±ν•˜μ§€λŠ” μ•Šμ•˜μŠ΅λ‹ˆλ‹€. OpenGLκ³ΌλŠ” 달리, Vulkanμ—μ„œλŠ” 밉맡을 μžλ™μœΌλ‘œ μƒμ„±ν•΄μ£ΌλŠ” 단일 호좜 ν•¨μˆ˜κ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠκΈ° λ•Œλ¬Έμ—, μš°λ¦¬κ°€ 직접 생성해주어야 ν•©λ‹ˆλ‹€.

create_imageλŠ” 이미 밉맡을 μ§€μ›ν•˜λ„λ‘ μž‘μ„±λ˜μ–΄ μžˆμ§€λ§Œ, μ‹€μ œ 데이터λ₯Ό μ—…λ‘œλ“œ ν•˜λŠ” 것을 μˆ˜μ •ν•˜μ—¬ 밉맡을 μƒμ„±ν•˜λ„λ‘ ν•΄μ•Ό ν•©λ‹ˆλ‹€. 이λ₯Ό μœ„ν•΄ ν•΄λ‹Ή ν•¨μˆ˜λ₯Ό λ³€κ²½ν•˜κ² μŠ΅λ‹ˆλ‹€.

AllocatedImage VulkanEngine::create_image(void* data, VkExtent3D size, VkFormat format, VkImageUsageFlags usage, bool mipmapped)
{
    size_t data_size = size.depth * size.width * size.height * 4;
    AllocatedBuffer uploadbuffer = create_buffer(data_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU);

    memcpy(uploadbuffer.info.pMappedData, data, data_size);

    AllocatedImage new_image = create_image(size, format, usage | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, mipmapped);

    immediate_submit([&](VkCommandBuffer cmd) {
        vkutil::transition_image(cmd, new_image.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);

        VkBufferImageCopy copyRegion = {};
        copyRegion.bufferOffset = 0;
        copyRegion.bufferRowLength = 0;
        copyRegion.bufferImageHeight = 0;

        copyRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
        copyRegion.imageSubresource.mipLevel = 0;
        copyRegion.imageSubresource.baseArrayLayer = 0;
        copyRegion.imageSubresource.layerCount = 1;
        copyRegion.imageExtent = size;

        // copy the buffer into the image
        vkCmdCopyBufferToImage(cmd, uploadbuffer.buffer, new_image.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1,
            &copyRegion);

        if (mipmapped) {
            vkutil::generate_mipmaps(cmd, new_image.image,VkExtent2D{new_image.imageExtent.width,new_image.imageExtent.height});
        } else {
            vkutil::transition_image(cmd, new_image.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
        }
    });
    destroy_buffer(uploadbuffer);
    return new_image;
}

immediate_submitλŠ” 이제 ν•΄λ‹Ή 이미지에 밉맡을 μ‚¬μš©ν•˜κ³  싢을 경우 vkutil::generate_mipmaps()ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν•  수 μžˆμŠ΅λ‹ˆλ‹€. 이 ν•¨μˆ˜λ₯Ό vk_images.h에 μΆ”κ°€ν•©μ‹œλ‹€.

namespace vkutil {
void generate_mipmaps(VkCommandBuffer cmd, VkImage image, VkExtent2D imageSize);
}

밉맡을 μƒμ„±ν•˜λŠ” λ°©λ²•μ—λŠ” μ—¬λŸ¬ κ°€μ§€κ°€ μžˆμŠ΅λ‹ˆλ‹€. λ°˜λ“œμ‹œ λΆˆλŸ¬μ˜€λŠ” μ‹œμ μ— 생성할 ν•„μš”λŠ” μ—†μœΌλ©°, 밉맡이 사전 μƒμ„±λ˜μ–΄ μžˆλŠ” KTX ν˜Ήμ€ DDS 같은 포맷을 μ‚¬μš©ν•˜λŠ” 것도 κ°€λŠ₯ν•©λ‹ˆλ‹€. κ°€μž₯ 많이 μ‚¬μš©λ˜λŠ” 방법 쀑 ν•˜λ‚˜λŠ” μ»΄ν“¨νŠΈ 셰이더λ₯Ό μ΄μš©ν•΄ μ—¬λŸ¬ λ ˆλ²¨μ„ ν•œ λ²ˆμ— μƒμ„±ν•˜λŠ” 방식이며, μ΄λŠ” μ„±λŠ₯ ν–₯상에 도움이 될 수 μžˆμŠ΅λ‹ˆλ‹€. μš°λ¦¬κ°€ μ‚¬μš©ν•˜λŠ” 방식은 VkCmdImageBlit ν˜ΈμΆœμ„ μ—°μ†μœΌλ‘œ μˆ˜ν–‰ν•˜μ—¬ 밉맡을 μƒμ„±ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€

각 λ°‰λ§΅ λ ˆλ²¨μ— λŒ€ν•΄, 이전 레벨의 이미지λ₯Ό λ‹€μŒ 레벨둜 λ³΅μ‚¬ν•˜λ©΄μ„œ 해상도λ₯Ό 절반으둜 쀄여야 ν•©λ‹ˆλ‹€. 이 κ³Όμ •μ—μ„œ 볡사 원본인 λ°‰λ§΅ 레벨의 λ ˆμ΄μ•„μ›ƒμ„ VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL둜 μ „ν™˜ν•΄μ•Ό ν•©λ‹ˆλ‹€. λͺ¨λ“  볡사가 μ™„λ£Œλ˜λ©΄ 전체 λ°‰λ§΅ λ ˆλ²¨μ— λŒ€ν•΄ 또 λ‹€λ₯Έ 배리어λ₯Ό μΆ”κ°€ν•˜κ³ , μ΄λ―Έμ§€μ˜ λ ˆμ΄μ•„μ›ƒμ„ VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL둜 μ „ν™˜ν•΄μ•Ό ν•©λ‹ˆλ‹€.

μ˜μ‚¬ μ½”λ“œλŠ” λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

//image already comes with layout VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL on all mipmap levels from image creation

int miplevels = calculate_mip_levels(imageSize);
for (int mip = 0; mip < mipLevels; mip++) {

    barrier( image.mips[mip] , VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL)

    //not the last level
    if (mip < mipLevels - 1)
    {
        copy_image(image.mips[mip], image.mips[mip+1];)
    }
}

barrier( image , VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)

μ‹€μ œ μ½”λ“œλ₯Ό μ‚΄νŽ΄λ΄…μ‹œλ‹€.

void vkutil::generate_mipmaps(VkCommandBuffer cmd, VkImage image, VkExtent2D imageSize)
{
    int mipLevels = int(std::floor(std::log2(std::max(imageSize.width, imageSize.height)))) + 1;
    for (int mip = 0; mip < mipLevels; mip++) {

        VkExtent2D halfSize = imageSize;
        halfSize.width /= 2;
        halfSize.height /= 2;

        VkImageMemoryBarrier2 imageBarrier { .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, .pNext = nullptr };

        imageBarrier.srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT;
        imageBarrier.srcAccessMask = VK_ACCESS_2_MEMORY_WRITE_BIT;
        imageBarrier.dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT;
        imageBarrier.dstAccessMask = VK_ACCESS_2_MEMORY_WRITE_BIT | VK_ACCESS_2_MEMORY_READ_BIT;

        imageBarrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
        imageBarrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;

        VkImageAspectFlags aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
        imageBarrier.subresourceRange = vkinit::image_subresource_range(aspectMask);
        imageBarrier.subresourceRange.levelCount = 1;
        imageBarrier.subresourceRange.baseMipLevel = mip;
        imageBarrier.image = image;

        VkDependencyInfo depInfo { .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .pNext = nullptr };
        depInfo.imageMemoryBarrierCount = 1;
        depInfo.pImageMemoryBarriers = &imageBarrier;

        vkCmdPipelineBarrier2(cmd, &depInfo);

        if (mip < mipLevels - 1) {
            VkImageBlit2 blitRegion { .sType = VK_STRUCTURE_TYPE_IMAGE_BLIT_2, .pNext = nullptr };

            blitRegion.srcOffsets[1].x = imageSize.width;
            blitRegion.srcOffsets[1].y = imageSize.height;
            blitRegion.srcOffsets[1].z = 1;

            blitRegion.dstOffsets[1].x = halfSize.width;
            blitRegion.dstOffsets[1].y = halfSize.height;
            blitRegion.dstOffsets[1].z = 1;

            blitRegion.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
            blitRegion.srcSubresource.baseArrayLayer = 0;
            blitRegion.srcSubresource.layerCount = 1;
            blitRegion.srcSubresource.mipLevel = mip;

            blitRegion.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
            blitRegion.dstSubresource.baseArrayLayer = 0;
            blitRegion.dstSubresource.layerCount = 1;
            blitRegion.dstSubresource.mipLevel = mip + 1;

            VkBlitImageInfo2 blitInfo {.sType = VK_STRUCTURE_TYPE_BLIT_IMAGE_INFO_2, .pNext = nullptr};
            blitInfo.dstImage = image;
            blitInfo.dstImageLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
            blitInfo.srcImage = image;
            blitInfo.srcImageLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
            blitInfo.filter = VK_FILTER_LINEAR;
            blitInfo.regionCount = 1;
            blitInfo.pRegions = &blitRegion;

            vkCmdBlitImage2(cmd, &blitInfo);

            imageSize = halfSize;
        }
    }

    // transition all mip levels into the final read_only layout
    transition_image(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
}

λ°°λ¦¬μ–΄λŠ” transition_imageμ—μ„œμ™€ μœ μ‚¬ν•˜λ©°, blit λ˜ν•œ copy_image_to_imageμ—μ„œ ν–ˆλ˜ 것과 μœ μ‚¬ν•˜μ§€λ§Œ, λ°‰λ§΅ λ ˆλ²¨μ„ μ²˜λ¦¬ν•œλ‹€λŠ” 점만 λ‹€λ¦…λ‹ˆλ‹€. 이 ν•¨μˆ˜λŠ” 두 κΈ°λŠ₯을 κ²°ν•©ν•œ ν˜•νƒœλΌκ³  λ³Ό 수 μžˆμŠ΅λ‹ˆλ‹€

각 λ°˜λ³΅λ¬Έμ—μ„œλŠ” 이미지 크기λ₯Ό 절반으둜 λ‚˜λˆ„μ–΄ κ°€λ©°, 볡사할 λ°‰λ§΅ λ ˆλ²¨μ„ μ „ν™˜ν•˜κ³ , ν˜„μž¬ λ°‰λ§΅ λ ˆλ²¨μ—μ„œ λ‹€μŒ 레벨둜 VkCmdBlit을 μˆ˜ν–‰ν•©λ‹ˆλ‹€.

vk_loader.cpp의 load_image에 μžˆλŠ” create_image 호좜 μ‹œ λ§ˆμ§€λ§‰ 인자λ₯Ό true둜 μ„€μ •ν–ˆλŠ”μ§€ ν™•μΈν•˜μ—¬, 밉맡이 μ‹€μ œλ‘œ μƒμ„±λ˜λ„λ‘ ν•΄μ£Όμ„Έμš”.

μ΄λ ‡κ²Œ ν•˜λ©΄ ν•„μš”ν•œ 밉맡이 μžλ™μœΌλ‘œ μƒμ„±λ©λ‹ˆλ‹€. 이미 μƒ˜ν”ŒλŸ¬λŠ” μ˜¬λ°”λ₯Έ μ„€μ •μœΌλ‘œ μƒμ„±λ˜μ–΄ μžˆμœΌλ―€λ‘œ, λ³„λ„μ˜ μˆ˜μ • 없이 잘 μž‘λ™ν•  κ²ƒμž…λ‹ˆλ‹€.