375 lines
13 KiB
Markdown
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)를 참고하여 각 함수의 세부 내용을 채워야 합니다.
|