# Android에서 Vulkan으로 텍스처 매핑된 사각형 렌더링하기 이 문서는 Android 환경에서 C/C++와 Vulkan 1.1 API를 사용하여 이미지 텍스처가 적용된 사각형을 렌더링하는 기본 예제를 설명합니다. 최종 결과물은 간단한 이미지 뷰어와 유사한 형태가 됩니다. ## 1. 개요 Android Native Activity와 Vulkan을 사용하여 다음 단계를 거쳐 렌더링을 수행합니다. 1. **Vulkan 기본 설정:** 인스턴스, 디바이스, 스왑체인, 렌더패스를 설정합니다. 2. **그래픽 파이프라인 생성:** Vertex/Fragment 셰이더를 로드하고, 렌더링 상태를 정의하여 파이프라인을 구축합니다. 3. **리소스 생성:** * 사각형을 그리기 위한 Vertex/Index 버퍼를 생성합니다. * 사각형에 입힐 이미지 텍스처, 이미지 뷰, 샘플러를 생성합니다. * 셰이더에 텍스처를 전달하기 위한 Descriptor Set을 설정합니다. 4. **렌더링 루프:** 매 프레임마다 화면을 그리고, 스왑체인에 제출하여 디스플레이에 표시합니다. --- ## 2. 프로젝트 구조 Android NDK 프로젝트의 `app/src/main/cpp` 폴더 내에 다음과 같은 파일들이 필요합니다. ``` app/src/main/cpp/ ├── CMakeLists.txt ├── main.cpp └── shaders/ ├── shader.frag └── shader.vert ``` --- ## 3. 빌드 설정 (CMakeLists.txt) 먼저 `CMakeLists.txt` 파일을 작성하여 네이티브 라이브러리를 빌드하고 Vulkan 및 Android 라이브러리와 연결합니다. ```cmake cmake_minimum_required(VERSION 3.10) project(VulkanAndroidExample) # 셰이더 파일을 컴파일하여 C 헤더로 변환 (glslc 필요) # 이 예제에서는 런타임에 파일을 읽는 방식으로 단순화합니다. # find_package(glslc) add_library( native-lib SHARED main.cpp ) # Android 시스템 라이브러리 찾기 find_library(log-lib log) find_library(android-lib android) # Vulkan 라이브러리 찾기 # Android NDK r21 이상에서는 기본적으로 포함되어 있습니다. find_library(vulkan-lib vulkan) if (NOT vulkan-lib) message(FATAL_ERROR "Vulkan library not found. Please use NDK r21 or newer.") endif() # 라이브러리 링크 target_link_libraries( native-lib ${log-lib} ${android-lib} ${vulkan-lib} ) ``` --- ## 4. 셰이더 코드 (GLSL) ### Vertex Shader (`shaders/shader.vert`) 버텍스의 위치(position)와 텍스처 좌표(uv)를 입력받아 프래그먼트 셰이더로 전달합니다. ```glsl #version 450 layout(location = 0) in vec2 inPosition; layout(location = 1) in vec2 inTexCoord; layout(location = 0) out vec2 fragTexCoord; void main() { gl_Position = vec4(inPosition, 0.0, 1.0); fragTexCoord = inTexCoord; } ``` ### Fragment Shader (`shaders/shader.frag`) 전달받은 텍스처 좌표를 사용하여 텍스처에서 색상 값을 샘플링하고 최종 색상으로 출력합니다. ```glsl #version 450 layout(location = 0) in vec2 fragTexCoord; layout(binding = 1) uniform sampler2D texSampler; layout(location = 0) out vec4 outColor; void main() { outColor = texture(texSampler, fragTexCoord); } ``` --- ## 5. C++ 구현 (main.cpp) 전체 Vulkan 렌더링 로직을 포함하는 메인 파일입니다. 코드가 길기 때문에 주요 함수와 로직 위주로 설명합니다. 실제 구현 시에는 각 함수의 세부 내용과 오류 처리를 추가해야 합니다. ```cpp #include #include #include #include #include #include // --- 데이터 구조 정의 --- struct Vertex { float pos[2]; float uv[2]; }; const std::vector vertices = { {{-0.8f, -0.8f}, {0.0f, 0.0f}}, {{0.8f, -0.8f}, {1.0f, 0.0f}}, {{0.8f, 0.8f}, {1.0f, 1.0f}}, {{-0.8f, 0.8f}, {0.0f, 1.0f}} }; const std::vector indices = { 0, 1, 2, 2, 3, 0 }; struct VulkanContext { ANativeWindow* window; VkInstance instance; VkDebugUtilsMessengerEXT debugMessenger; VkSurfaceKHR surface; VkPhysicalDevice physicalDevice; VkDevice device; VkQueue graphicsQueue; VkSwapchainKHR swapchain; std::vector swapchainImages; std::vector swapchainImageViews; VkRenderPass renderPass; VkDescriptorSetLayout descriptorSetLayout; VkPipelineLayout pipelineLayout; VkPipeline graphicsPipeline; std::vector swapchainFramebuffers; VkCommandPool commandPool; std::vector commandBuffers; // 텍스처 리소스 VkImage textureImage; VkDeviceMemory textureImageMemory; VkImageView textureImageView; VkSampler textureSampler; // Vertex/Index 버퍼 VkBuffer vertexBuffer; VkDeviceMemory vertexBufferMemory; VkBuffer indexBuffer; VkDeviceMemory indexBufferMemory; VkDescriptorPool descriptorPool; VkDescriptorSet descriptorSet; // 동기화 객체 std::vector imageAvailableSemaphores; std::vector renderFinishedSemaphores; std::vector inFlightFences; uint32_t currentFrame = 0; }; // --- 주요 함수 프로토타입 --- void initVulkan(android_app* app, VulkanContext& context); void createTextureImage(VulkanContext& context); void createGraphicsPipeline(VulkanContext& context, AAssetManager* assetManager); void createVertexBuffer(VulkanContext& context); void createIndexBuffer(VulkanContext& context); void createDescriptorSets(VulkanContext& context); void drawFrame(VulkanContext& context); void cleanup(VulkanContext& context); std::vector readFile(AAssetManager* assetManager, const std::string& filename); uint32_t findMemoryType(VulkanContext& context, uint32_t typeFilter, VkMemoryPropertyFlags properties); // --- Android 앱 메인 함수 --- void android_main(struct android_app* app) { VulkanContext context = {}; // 이벤트 루프를 위한 콜백 설정 app->userData = &context; // app->onAppCmd = handle_cmd; // (생략) 이벤트 핸들러 구현 필요 // 앱이 초기화되고 윈도우가 생성될 때까지 대기 // 실제 구현에서는 이벤트 루프 내에서 처리해야 함 // 여기서는 단순화를 위해 기본 흐름만 표시 // [가정] 윈도우가 준비되었다고 가정 // context.window = app->window; initVulkan(app, context); createVertexBuffer(context); createIndexBuffer(context); createTextureImage(context); createGraphicsPipeline(context, app->activity->assetManager); createDescriptorSets(context); // 메인 렌더링 루프 while (true) { int events; struct android_poll_source* source; // 이벤트 처리 if (ALooper_pollAll(0, nullptr, &events, (void**)&source) >= 0) { if (source != nullptr) { source->process(app, source); } } // 앱이 종료 요청되면 루프 탈출 if (app->destroyRequested != 0) { break; } drawFrame(context); } vkDeviceWaitIdle(context.device); cleanup(context); } // --- 핵심 구현 (일부 함수는 개념만 설명) --- void initVulkan(android_app* app, VulkanContext& context) { // 1. VkInstance 생성 // 2. Validation Layer 설정 (디버그 빌드 시) // 3. VkSurfaceKHR 생성 (ANativeWindow로부터) // 4. VkPhysicalDevice 선택 // 5. VkDevice 및 VkQueue 생성 // 6. VkSwapchainKHR 생성 // 7. VkImageView 생성 // 8. VkRenderPass 생성 // 9. VkCommandPool 생성 // 10. VkFramebuffer 생성 // 11. 동기화 객체(Semaphore, Fence) 생성 // ... (Vulkan의 표준 초기화 절차) } void createTextureImage(VulkanContext& context) { // 1. 텍스처 데이터 생성 (예: 2x2 흑백 체크무늬) uint8_t pixels[4 * 4] = { 0, 0, 0, 255, // Black 255, 255, 255, 255, // White 255, 255, 255, 255, // White 0, 0, 0, 255, // Black }; VkDeviceSize imageSize = 4 * 4; int texWidth = 2, texHeight = 2; // 2. Staging 버퍼 생성 및 데이터 복사 VkBuffer stagingBuffer; VkDeviceMemory stagingBufferMemory; // ... createBuffer, allocateMemory, mapMemory, memcpy, unmapMemory ... // 3. VkImage 생성 (VK_IMAGE_TILING_OPTIMAL, USAGE_TRANSFER_DST | USAGE_SAMPLED) // ... vkCreateImage ... // 4. VkDeviceMemory 할당 및 바인딩 // ... vkAllocateMemory, vkBindImageMemory ... // 5. 이미지 레이아웃 변경 (UNDEFINED -> TRANSFER_DST_OPTIMAL) // ... transitionImageLayout ... // 6. Staging 버퍼에서 VkImage로 데이터 복사 // ... copyBufferToImage ... // 7. 이미지 레이아웃 변경 (TRANSFER_DST_OPTIMAL -> SHADER_READ_ONLY_OPTIMAL) // ... transitionImageLayout ... // 8. Staging 버퍼 정리 vkDestroyBuffer(context.device, stagingBuffer, nullptr); vkFreeMemory(context.device, stagingBufferMemory, nullptr); // 9. VkImageView 생성 // ... vkCreateImageView ... // 10. VkSampler 생성 // ... vkCreateSampler ... } void createGraphicsPipeline(VulkanContext& context, AAssetManager* assetManager) { auto vertShaderCode = readFile(assetManager, "shaders/shader.vert.spv"); // SPV로 사전 컴파일 필요 auto fragShaderCode = readFile(assetManager, "shaders/shader.frag.spv"); // 1. 셰이더 모듈 생성 // 2. Vertex Input State 설정 (Vertex 구조체에 맞게) // 3. Input Assembly, Viewport, Rasterizer, Multisample, DepthStencil, ColorBlend 상태 설정 // 4. DescriptorSetLayout 생성 (Uniform Sampler를 위해) // 5. Pipeline Layout 생성 // 6. VkGraphicsPipelineCreateInfo 채우기 // 7. vkCreateGraphicsPipelines 호출 // ... (Vulkan의 표준 파이프라인 생성 절차) } void createDescriptorSets(VulkanContext& context) { // 1. VkDescriptorPool 생성 (Sampler 타입 1개) // 2. VkDescriptorSetAllocateInfo로 Descriptor Set 할당 // 3. VkDescriptorImageInfo 설정 (textureImageView, textureSampler) // 4. VkWriteDescriptorSet 구조체 채우기 // 5. vkUpdateDescriptorSets 호출하여 셰이더의 `texSampler`와 실제 텍스처를 연결 } void drawFrame(VulkanContext& context) { // 1. vkAcquireNextImageKHR로 스왑체인에서 렌더링할 이미지 인덱스 획득 // 2. 현재 프레임의 Command Buffer 리셋 // 3. Command Buffer 기록 시작 (vkBeginCommandBuffer) // 4. Render Pass 시작 (vkCmdBeginRenderPass) // 5. 그래픽 파이프라인 바인딩 (vkCmdBindPipeline) // 6. Vertex/Index 버퍼 바인딩 // 7. Descriptor Set 바인딩 (vkCmdBindDescriptorSets) // 8. 드로우 콜 (vkCmdDrawIndexed) // 9. Render Pass 종료 // 10. Command Buffer 기록 종료 // 11. vkQueueSubmit으로 Command Buffer 제출 (동기화 Semaphore 포함) // 12. vkQueuePresentKHR으로 결과물을 화면에 표시 } // --- 유틸리티 함수 --- std::vector readFile(AAssetManager* assetManager, const std::string& filename) { AAsset* file = AAssetManager_open(assetManager, filename.c_str(), AASSET_MODE_BUFFER); size_t fileLength = AAsset_getLength(file); std::vector buffer(fileLength); AAsset_read(file, buffer.data(), fileLength); AAsset_close(file); return buffer; } // ... 기타 필요한 유틸리티 함수들 (findMemoryType, createBuffer 등) ``` ## 6. 실행 방법 1. **GLSL 셰이더 컴파일:** Vulkan SDK에 포함된 `glslc` 컴파일러를 사용하여 `.vert`와 `.frag` 파일을 SPIR-V 형식(`.spv`)으로 컴파일해야 합니다. ```bash glslc shaders/shader.vert -o app/src/main/assets/shaders/shader.vert.spv glslc shaders/shader.frag -o app/src/main/assets/shaders/shader.frag.spv ``` 컴파일된 셰이더는 Android의 `assets` 폴더에 위치시켜 런타임에 `AAssetManager`로 읽을 수 있도록 합니다. 2. **Android Studio 프로젝트 빌드:** 위 파일들을 포함하는 Android Studio 프로젝트를 생성하고 NDK를 설정한 후 빌드합니다. `main.cpp`의 생략된 부분(Vulkan 초기화, 리소스 생성/해제 등)을 상세히 구현해야 합니다. 3. **실행:** Vulkan을 지원하는 Android 기기나 에뮬레이터에서 앱을 실행하면, 화면 중앙에 흑백 체크무늬 텍스처가 입혀진 사각형이 나타납니다. --- **참고:** 위 `main.cpp` 코드는 전체 구조와 핵심 로직을 보여주기 위한 의사 코드에 가깝습니다. Vulkan은 각 단계가 매우 상세하며 수많은 설정과 오류 처리가 필요합니다. 실제 구현 시에는 [Vulkan Tutorial](https://vulkan-tutorial.com/) 사이트나 Sascha Willems의 [Vulkan-Samples](https://github.com/SaschaWillems/Vulkan)를 참고하여 각 함수의 세부 내용을 채워야 합니다.