13 KiB
Android에서 Vulkan으로 텍스처 매핑된 사각형 렌더링하기
이 문서는 Android 환경에서 C/C++와 Vulkan 1.1 API를 사용하여 이미지 텍스처가 적용된 사각형을 렌더링하는 기본 예제를 설명합니다. 최종 결과물은 간단한 이미지 뷰어와 유사한 형태가 됩니다.
1. 개요
Android Native Activity와 Vulkan을 사용하여 다음 단계를 거쳐 렌더링을 수행합니다.
- Vulkan 기본 설정: 인스턴스, 디바이스, 스왑체인, 렌더패스를 설정합니다.
- 그래픽 파이프라인 생성: Vertex/Fragment 셰이더를 로드하고, 렌더링 상태를 정의하여 파이프라인을 구축합니다.
- 리소스 생성:
- 사각형을 그리기 위한 Vertex/Index 버퍼를 생성합니다.
- 사각형에 입힐 이미지 텍스처, 이미지 뷰, 샘플러를 생성합니다.
- 셰이더에 텍스처를 전달하기 위한 Descriptor Set을 설정합니다.
- 렌더링 루프: 매 프레임마다 화면을 그리고, 스왑체인에 제출하여 디스플레이에 표시합니다.
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_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)를 입력받아 프래그먼트 셰이더로 전달합니다.
#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)
전달받은 텍스처 좌표를 사용하여 텍스처에서 색상 값을 샘플링하고 최종 색상으로 출력합니다.
#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 렌더링 로직을 포함하는 메인 파일입니다. 코드가 길기 때문에 주요 함수와 로직 위주로 설명합니다. 실제 구현 시에는 각 함수의 세부 내용과 오류 처리를 추가해야 합니다.
#include <android/native_activity.h>
#include <android/asset_manager.h>
#include <android_native_app_glue.h>
#include <vulkan/vulkan.h>
#include <vector>
#include <string>
// --- 데이터 구조 정의 ---
struct Vertex {
float pos[2];
float uv[2];
};
const std::vector<Vertex> 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<uint16_t> 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<VkImage> swapchainImages;
std::vector<VkImageView> swapchainImageViews;
VkRenderPass renderPass;
VkDescriptorSetLayout descriptorSetLayout;
VkPipelineLayout pipelineLayout;
VkPipeline graphicsPipeline;
std::vector<VkFramebuffer> swapchainFramebuffers;
VkCommandPool commandPool;
std::vector<VkCommandBuffer> 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<VkSemaphore> imageAvailableSemaphores;
std::vector<VkSemaphore> renderFinishedSemaphores;
std::vector<VkFence> 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<char> 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<char> 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<char> buffer(fileLength);
AAsset_read(file, buffer.data(), fileLength);
AAsset_close(file);
return buffer;
}
// ... 기타 필요한 유틸리티 함수들 (findMemoryType, createBuffer 등)
6. 실행 방법
-
GLSL 셰이더 컴파일: Vulkan SDK에 포함된
glslc컴파일러를 사용하여.vert와.frag파일을 SPIR-V 형식(.spv)으로 컴파일해야 합니다.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로 읽을 수 있도록 합니다. -
Android Studio 프로젝트 빌드: 위 파일들을 포함하는 Android Studio 프로젝트를 생성하고 NDK를 설정한 후 빌드합니다.
main.cpp의 생략된 부분(Vulkan 초기화, 리소스 생성/해제 등)을 상세히 구현해야 합니다. -
실행: Vulkan을 지원하는 Android 기기나 에뮬레이터에서 앱을 실행하면, 화면 중앙에 흑백 체크무늬 텍스처가 입혀진 사각형이 나타납니다.
참고: 위 main.cpp 코드는 전체 구조와 핵심 로직을 보여주기 위한 의사 코드에 가깝습니다. Vulkan은 각 단계가 매우 상세하며 수많은 설정과 오류 처리가 필요합니다. 실제 구현 시에는 Vulkan Tutorial 사이트나 Sascha Willems의 Vulkan-Samples를 참고하여 각 함수의 세부 내용을 채워야 합니다.