Files
video-v1/vav2/docs/example/Vulkan_Android_Texture_Example.md
2025-09-30 00:34:20 +09:00

375 lines
13 KiB
Markdown

# 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 <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. 실행 방법
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)를 참고하여 각 함수의 세부 내용을 채워야 합니다.