Android:将您的 Vulkan 应用带到移动端
简介
在上一章中,我们探讨了 Vulkan 配置文件如何简化功能检测并使代码更易于维护。现在,让我们通过将 Vulkan 应用带到 Android 移动端,进一步扩展我们的 Vulkan 知识。
虽然 Vulkan 从一开始就设计为跨平台,但在 Android 上部署会带来一些新的挑战和机遇。核心 Vulkan API 保持不变,但周围的生态系统(从窗口管理到构建系统)需要不同的方法。
本章将指导您如何将 Vulkan 应用适配到 Android,尽可能重用代码,同时解决平台特定的需求。您将看到,通过正确的设置,可以维护一个适用于桌面和移动平台的单一代码库。
Android 特定注意事项
在深入实现细节之前,让我们先了解开发 Vulkan 应用在 Android 上与桌面平台的主要区别:
-
窗口系统集成:不再使用 GLFW,而是使用 Android 的原生窗口系统和活动生命周期。
-
应用生命周期:Android 应用可以随时被系统暂停、恢复或终止,需要谨慎管理资源。
-
资源加载:资源打包在 APK 文件中,通过 Android 的资产管理器访问。
-
构建系统:我们使用 Gradle 和 CMake 共同构建 Android 应用。
-
输入处理:触摸输入取代了鼠标和键盘,需要不同的事件处理方式。
这些差异乍一看可能令人望而生畏,但通过正确的方法,我们可以在保持代码整洁和可维护性的同时解决这些问题。
项目设置
现在我们已经了解了关键差异,接下来设置我们的 Android 项目。我们的目标是尽可能重用桌面实现的代码,同时满足 Android 特定的需求。
先决条件
在开始之前,请确保已安装以下工具:
-
Android Studio:Android 开发的官方 IDE
-
Android NDK(原生开发工具包):支持在 Android 上进行原生 C++ 开发
-
Android SDK:使用较新的 API 级别(24+,对应 Android 7.0 或更高版本)以支持 Vulkan
-
CMake 和 Ninja 构建工具:用于构建原生代码(可通过 Android Studio 安装)
-
Vulkan SDK:用于着色器编译工具和验证层
|
与桌面环境不同,Vulkan HPP(Vulkan 的 C++ 绑定)默认不包含在 Android NDK 中。您需要从 Vulkan-Hpp GitHub 仓库 单独下载,或使用 Vulkan SDK 中包含的版本。 |
项目结构
让我们从了解 Android 项目的结构开始。我们将遵循标准的 Android 应用结构,但进行一些修改以高效地重用主项目中的代码:
android/
├── app/
│ ├── build.gradle // 应用级构建配置
│ ├── src/
│ │ ├── main/
│ │ │ ├── AndroidManifest.xml // 应用清单
│ │ │ ├── cpp/ // 原生代码
│ │ │ │ ├── CMakeLists.txt // CMake 构建脚本
│ │ │ │ └── game_activity_bridge.cpp // GameActivity 与 Vulkan 代码的桥梁
│ │ │ ├── java/ // Java 代码
│ │ │ │ └── com/example/vulkantutorial/
│ │ │ │ └── VulkanActivity.java // 主活动(继承自 GameActivity)
│ │ │ └── res/ // 资源
│ │ │ └── values/
│ │ │ ├── strings.xml // 字符串资源
│ │ │ └── styles.xml // 样式资源
├── build.gradle // 项目级构建配置
├── gradle/ // Gradle 包装器
├── settings.gradle // 项目设置
设置 Android 项目
有了项目结构后,让我们深入了解 Android Vulkan 应用的关键组件。我们将从基本配置文件开始,然后转向原生代码实现。
清单文件
每个 Android 应用都需要一个清单文件来声明应用的重要信息。对于 Vulkan 应用,AndroidManifest.xml 文件尤为重要,因为它指定了 Vulkan 版本要求:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.vulkan.tutorial">
<!-- Vulkan 需要 API 级别 24(Android 7.0)或更高 -->
<uses-sdk android:minSdkVersion="24" />
<!-- 声明此应用使用 Vulkan -->
<uses-feature android:name="android.hardware.vulkan.version" android:version="0x400003" android:required="true" />
<uses-feature android:name="android.hardware.vulkan.level" android:version="0" android:required="true" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".VulkanActivity"
android:label="@string/app_name"
android:configChanges="orientation|keyboardHidden|screenSize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
关键点:
-
我们指定最低 SDK 版本为 24(Android 7.0),这是 Vulkan 支持的最低要求。
-
我们声明应用使用 Vulkan,并指定版本要求。
-
我们设置主活动(VulkanActivity)为应用的入口点。
Java 活动
配置清单后,我们需要创建应用的 Java 部分。虽然大部分 Vulkan 代码将在原生 C++ 中运行,但我们仍然需要一个 Java 活动作为应用的入口点。
对于 Vulkan 应用,我们将使用 Android Game SDK 中的 GameActivity,而不是传统的 NativeActivity。这种现代方法提供了更好的性能,并专为游戏和图形密集型应用设计:
package com.vulkan.tutorial;
import android.os.Bundle;
import android.view.WindowManager;
import com.google.androidgamesdk.GameActivity;
public class VulkanActivity extends GameActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 应用运行时保持屏幕常亮
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
// 加载原生库
static {
System.loadLibrary("vulkan_tutorial_android");
}
}
关键点:
-
我们继承自 Android Game SDK 中的 GameActivity,它提供了 Java 和原生代码之间更优化的桥梁。
-
GameActivity 为游戏和图形密集型应用提供了比 NativeActivity 更好的性能。
-
我们加载包含 Vulkan 实现的原生库("vulkan_tutorial_android")。
构建配置
有了 Java 活动后,我们需要配置构建过程。Android 使用 Gradle 作为构建系统,我们将配置它以与原生 Vulkan 代码和资源协同工作。
构建配置分布在多个文件中,各自有不同的职责:
项目级 build.gradle:
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.2'
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
应用级 build.gradle:
plugins {
id 'com.android.application'
}
android {
compileSdkVersion 33
defaultConfig {
applicationId "com.vulkan.tutorial"
minSdkVersion 24
targetSdkVersion 33
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.22.1"
}
}
ndkVersion "25.2.9519653"
// 使用主项目的资源和本地编译的着色器
sourceSets {
main {
assets {
srcDirs = [
// 指向主项目的资源
'../../../../', // 附件目录中的模型和纹理
// 使用构建目录中本地编译的着色器
'.externalNativeBuild/cmake/debug/arm64-v8a/shaders',
'.externalNativeBuild/cmake/debug/armeabi-v7a/shaders',
'.externalNativeBuild/cmake/debug/x86/shaders',
'.externalNativeBuild/cmake/debug/x86_64/shaders',
// 也包括发布构建路径
'.externalNativeBuild/cmake/release/arm64-v8a/shaders',
'.externalNativeBuild/cmake/release/armeabi-v7a/shaders',
'.externalNativeBuild/cmake/release/x86/shaders',
'.externalNativeBuild/cmake/release/x86_64/shaders'
]
}
}
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'com.google.androidgamesdk:game-activity:1.2.0'
}
关键点:
-
我们指定最低 SDK 版本为 24(Android 7.0)以支持 Vulkan。
-
我们配置 CMake 来构建原生代码。
-
我们包含 game-activity 依赖以获得更好的性能。
-
我们设置资源目录以引用主项目的资源和本地编译的着色器。
-
这种方法避免了资源重复,并确保我们使用最新版本。
CMake 配置
虽然 Gradle 处理整个 Android 构建过程,但我们使用 CMake 来构建原生 C++ 代码。在这里,我们将设置 Vulkan 环境、编译着色器并链接必要的库。
让我们检查 CMakeLists.txt 文件,这是我们原生代码配置的核心:
cmake_minimum_required(VERSION 3.22.1)
project(vulkan_tutorial_android)
# 设置主 CMakeLists.txt 的路径
set(MAIN_CMAKE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../CMakeLists.txt")
# 查找 Vulkan 包
find_package(Vulkan REQUIRED)
# 设置着色器编译工具
add_executable(glslang::validator IMPORTED)
find_program(GLSLANG_VALIDATOR "glslangValidator" HINTS $ENV{VULKAN_SDK}/bin REQUIRED)
set_property(TARGET glslang::validator PROPERTY IMPORTED_LOCATION "${GLSLANG_VALIDATOR}")
# 定义着色器构建函数
function(add_shaders_target TARGET)
cmake_parse_arguments("SHADER" "" "CHAPTER_NAME" "SOURCES" ${ARGN})
set(SHADERS_DIR ${SHADER_CHAPTER_NAME}/shaders)
add_custom_command(
OUTPUT ${SHADERS_DIR}
COMMAND ${CMAKE_COMMAND} -E make_directory ${SHADERS_DIR}
)
add_custom_command(
OUTPUT ${SHADERS_DIR}/frag.spv ${SHADERS_DIR}/vert.spv
COMMAND glslang::validator
ARGS --target-env vulkan1.0 ${SHADER_SOURCES} --quiet
WORKING_DIRECTORY ${SHADERS_DIR}
DEPENDS ${SHADERS_DIR} ${SHADER_SOURCES}
COMMENT "Compiling Shaders"
VERBATIM
)
add_custom_target(${TARGET} DEPENDS ${SHADERS_DIR}/frag.spv ${SHADERS_DIR}/vert.spv)
endfunction()
# 包含 game-activity 库
find_package(game-activity REQUIRED CONFIG)
include_directories(${ANDROID_NDK}/sources/android/game-activity/include)
# 设置 C++ 标准以匹配主项目
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 添加 Vulkan C++ 模块
add_library(VulkanCppModule SHARED)
target_compile_definitions(VulkanCppModule
PUBLIC VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1 VULKAN_HPP_NO_STRUCT_CONSTRUCTORS=1
)
target_include_directories(VulkanCppModule
PRIVATE
"${Vulkan_INCLUDE_DIR}"
)
target_link_libraries(VulkanCppModule
PUBLIC
${Vulkan_LIBRARIES}
)
set_target_properties(VulkanCppModule PROPERTIES CXX_STANDARD 20)
# 设置 C++ 模块文件集
target_sources(VulkanCppModule
PUBLIC
FILE_SET cxx_modules TYPE CXX_MODULES
BASE_DIRS
"${Vulkan_INCLUDE_DIR}"
FILES
"${Vulkan_INCLUDE_DIR}/vulkan/vulkan.cppm"
)
# 为 34_android 设置着色器编译
set(SHADER_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments")
set(SHADER_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/shaders")
file(MAKE_DIRECTORY ${SHADER_OUTPUT_DIR})
# 将着色器源文件复制到构建目录
configure_file(
"${SHADER_SOURCE_DIR}/27_shader_depth.frag"
"${SHADER_OUTPUT_DIR}/27_shader_depth.frag"
COPYONLY
)
configure_file(
"${SHADER_SOURCE_DIR}/27_shader_depth.vert"
"${SHADER_OUTPUT_DIR}/27_shader_depth.vert"
COPYONLY
)
# 编译着色器
set(SHADER_SOURCES "${SHADER_OUTPUT_DIR}/27_shader_depth.frag" "${SHADER_OUTPUT_DIR}/27_shader_depth.vert")
add_shaders_target(android_shaders CHAPTER_NAME "${SHADER_OUTPUT_DIR}" SOURCES ${SHADER_SOURCES})
# 添加主原生库
add_library(vulkan_tutorial_android SHARED
${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/34_android.cpp
game_activity_bridge.cpp
)
# 添加对着色器编译的依赖
add_dependencies(vulkan_tutorial_android android_shaders)
# 设置包含目录
target_include_directories(vulkan_tutorial_android PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${Vulkan_INCLUDE_DIR}
${ANDROID_NDK}/sources/android/game-activity/include
)
# 链接库
target_link_libraries(vulkan_tutorial_android
VulkanCppModule
game-activity::game-activity
android
log
${Vulkan_LIBRARIES}
)
关键点:
-
我们查找 Vulkan 包并包含 game-activity 库而不是 native_app_glue。
-
我们设置着色器编译工具并定义一个函数来编译着色器。
-
我们将 C 标准设置为 C20 并创建一个 Vulkan C++ 模块。
-
我们为 34_android 章节设置着色器编译,从主项目复制着色器源文件。
-
我们添加主原生库,它使用主项目中的 34_android.cpp 文件和一个桥接文件来连接 GameActivity。
-
我们链接必要的库,包括 game-activity。
原生实现
现在我们已经设置了构建配置,让我们深入了解为 Android 提供动力的原生 C++ 代码。这是真正的魔法发生的地方 - 我们将看到如何调整现有的 Vulkan 代码以在 Android 上工作,同时尽量减少平台特定的更改。
我们方法的一个关键优势是代码重用。我们不是为桌面和 Android 维护单独的代码库,而是构建项目以尽可能多地共享代码:
-
34_android.cpp:这是主项目中使用的相同文件,包含核心 Vulkan 实现。通过重用此文件,我们确保渲染代码在不同平台上保持一致。
-
game_activity_bridge.cpp:这个小型桥接文件连接 Android GameActivity 和我们的核心 Vulkan 代码。它处理平台特定的初始化和事件处理。
这种关注点分离使我们能够专注于 Vulkan 实现,而不会被平台特定的细节所困扰。当我们改进渲染代码时,桌面和 Android 版本都会自动受益。
GameActivity 桥接
让我们仔细看看我们的桥接代码,它是连接 Java GameActivity 和原生 Vulkan 实现的关键。这个虽小但至关重要的文件处理 Android 基于 Java 的活动生命周期与我们的 C++ 代码之间的转换:
#include <game-activity/GameActivity.h>
#include <game-activity/native_app_glue/android_native_app_glue.h>
#include <android/log.h>
// 定义日志宏
#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "VulkanTutorial", __VA_ARGS__))
#define LOGW(...) ((void)__android_log_print(ANDROID_LOG_WARN, "VulkanTutorial", __VA_ARGS__))
#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, "VulkanTutorial", __VA_ARGS__))
// 主入口点的前向声明
extern "C" void android_main(android_app* app);
// GameActivity 入口点
extern "C" {
void GameActivity_onCreate(GameActivity* activity) {
LOGI("GameActivity_onCreate");
// 创建 android_app 结构
android_app* app = new android_app();
memset(app, 0, sizeof(android_app));
// 设置 android_app 结构
app->activity = activity;
app->window = activity->window;
// 调用原始的 android_main 函数
android_main(app);
// 清理
delete app;
}
}
这段桥接代码:
-
创建一个与我们的 Vulkan 代码兼容的 android_app 结构
-
设置 GameActivity 和我们的代码之间的必要连接
-
调用 34_android.cpp 文件中的 android_main 函数
Android 入口点
一旦我们的桥接代码创建了 android_app 结构,它就会调用 android_main 函数,该函数作为我们原生代码的入口点。这个函数定义在我们的 34_android.cpp 文件中,类似于桌面应用中的 main() 函数:
让我们看看如何从这个入口点初始化 Vulkan 应用:
void android_main(android_app* app) {
try {
// 创建并运行 Vulkan 应用
HelloTriangleApplication application(app);
application.run();
} catch (const std::exception& e) {
LOGE("Exception caught: %s", e.what());
}
}
创建 Vulkan 表面
我们 Vulkan 实现中一个关键的平台特定差异是如何创建表面。在桌面上,我们使用 GLFW 创建窗口和表面。在 Android 上,我们需要使用 VK_KHR_android_surface 扩展从原生 Android 窗口创建表面。
以下是在 Android 上创建 Vulkan 表面的方法:
void createSurface() {
VkSurfaceKHR _surface;
VkResult result = VK_SUCCESS;
// 创建 Android 表面
result = vkCreateAndroidSurfaceKHR(
*instance,
&(VkAndroidSurfaceCreateInfoKHR{
.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR,
.pNext = nullptr,
.flags = 0,
.window = androidApp->window
}),
nullptr,
&_surface
);
if (result != VK_SUCCESS) {
throw std::runtime_error("Failed to create Android surface");
}
surface = vk::raii::SurfaceKHR(instance, _surface);
}
处理 Android 事件
另一个重要的平台特定方面是事件处理。Android 应用的生命周期与桌面应用不同 - 它们可以随时被系统暂停、恢复或终止。我们需要正确处理这些事件以确保 Vulkan 资源得到正确管理。
以下是我们如何处理应用中的 Android 特定事件:
static void handleAppCommand(android_app* app, int32_t cmd) {
auto* vulkanApp = static_cast<VulkanApplication*>(app->userData);
switch (cmd) {
case APP_CMD_INIT_WINDOW:
// 窗口创建,初始化 Vulkan
if (app->window != nullptr) {
vulkanApp->initVulkan();
}
break;
case APP_CMD_TERM_WINDOW:
// 窗口销毁,清理 Vulkan
vulkanApp->cleanup();
break;
default:
break;
}
}
static int32_t handleInputEvent(android_app* app, AInputEvent* event) {
auto* vulkanApp = static_cast<VulkanApplication*>(app->userData);
if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_MOTION) {
// 处理触摸事件
float x = AMotionEvent_getX(event, 0);
float y = AMotionEvent_getY(event, 0);
// 处理触摸坐标
// ...
return 1;
}
return 0;
}
跨平台实现
虽然到目前为止我们专注于 Android 特定的代码,但我们的方法允许我们维护一个适用于桌面和 Android 平台的单一代码库。这是通过谨慎使用预处理器指令和平台特定抽象来实现的。
平台检测
跨平台方法的第一步是检测我们正在为哪个平台构建。我们使用预处理器指令来检查平台特定的预定义宏:
// 平台检测
#if defined(__ANDROID__)
#define PLATFORM_ANDROID 1
#else
#define PLATFORM_DESKTOP 1
#endif
这种方法利用了标准预定义宏 ANDROID,当为 Android 平台构建时,编译器会自动定义它。这些平台宏随后在整个代码中使用,以有条件地编译平台特定的代码。
一致的类结构
为了保持代码库的整洁和一致,我们在两个平台上使用相同的类名(HelloTriangleApplication)。这使得代码更易于理解,并减少了对平台特定分支的需求:
// 跨平台应用类
class HelloTriangleApplication {
public:
#if PLATFORM_DESKTOP
// 桌面构造函数
HelloTriangleApplication() {
// 不需要 Android 特定的初始化
}
#else
// Android 构造函数
HelloTriangleApplication(android_app* app) : androidApp(app) {
// Android 特定的初始化
}
#endif
// ... 类的其余部分 ...
};
平台特定的包含
不同平台需要不同的头文件。我们使用预处理器指令来包含适当的头文件:
// 平台特定的包含
#if PLATFORM_ANDROID
// Android 特定的包含
#include <android/log.h>
#include <android_native_app_glue.h>
#include <android/asset_manager.h>
#include <android/asset_manager_jni.h>
#else
// 桌面特定的包含
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#include <stb_image.h>
#include <tiny_obj_loader.h>
#endif
跨平台文件加载
文件加载是桌面和 Android 平台之间的关键区别之一。在桌面上,我们从文件系统加载文件,而在 Android 上,我们从 APK 的资源中加载它们。我们创建了一个跨平台的文件加载函数,适用于两个平台:
// 跨平台文件读取函数
std::vector<char> readFile(const std::string& filename, std::optional<AAssetManager*> assetManager = std::nullopt) {
#if PLATFORM_ANDROID
// 在 Android 上,如果提供了资产管理器则使用它
if (assetManager.has_value() && *assetManager != nullptr) {
// 打开资产
AAsset* asset = AAssetManager_open(*assetManager, filename.c_str(), AASSET_MODE_BUFFER);
// ... 从资产读取文件 ...
return buffer;
}
#endif
// 桌面版本或 Android 回退到文件系统
std::ifstream file(filename, std::ios::ate | std::ios::binary);
// ... 从文件系统读取文件 ...
return buffer;
}
平台特定的入口点
每个平台都有自己的入口点。在桌面上,我们使用标准的 main() 函数,而在 Android 上,我们使用 android_main() 函数:
// 平台特定的入口点
#if PLATFORM_ANDROID
// Android 主入口点
void android_main(android_app* app) {
// Android 特定的初始化
try {
HelloTriangleApplication vulkanApp(app);
vulkanApp.run();
} catch (const std::exception& e) {
LOGE("Exception caught: %s", e.what());
}
}
#else
// 桌面主入口点
int main() {
try {
HelloTriangleApplication app;
app.run();
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
#endif
Android 上的着色器处理
现在我们已经介绍了核心原生实现,让我们讨论 Vulkan 开发在 Android 上的另一个重要方面:着色器处理。着色器是任何 Vulkan 应用的关键部分,我们需要确保它们在 Android 上正确编译和加载。
在我们的方法中,我们在构建过程中本地编译着色器,类似于主项目中的做法。这种策略提供了几个显著的优势:
-
一致性:我们为桌面和 Android 构建使用相同的着色器源文件,确保跨平台的视觉效果一致。
-
可维护性:当我们需要更新着色器时,只需在一个地方更改,桌面和 Android 版本都会受益。
-
构建时验证:着色器编译错误在构建过程中捕获,而不是在运行时,使调试更加容易。
本地着色器编译
我们已经设置了 CMake 配置以在构建过程中本地编译着色器:
-
定义着色器构建函数:
function(add_shaders_target TARGET) cmake_parse_arguments("SHADER" "" "CHAPTER_NAME" "SOURCES" ${ARGN}) set(SHADERS_DIR ${SHADER_CHAPTER_NAME}/shaders) add_custom_command( OUTPUT ${SHADERS_DIR} COMMAND ${CMAKE_COMMAND} -E make_directory ${SHADERS_DIR} ) add_custom_command( OUTPUT ${SHADERS_DIR}/frag.spv ${SHADERS_DIR}/vert.spv COMMAND glslang::validator ARGS --target-env vulkan1.0 ${SHADER_SOURCES} --quiet WORKING_DIRECTORY ${SHADERS_DIR} DEPENDS ${SHADERS_DIR} ${SHADER_SOURCES} COMMENT "Compiling Shaders" VERBATIM ) add_custom_target(${TARGET} DEPENDS ${SHADERS_DIR}/frag.spv ${SHADERS_DIR}/vert.spv) endfunction() -
从主项目复制着色器源文件:
# 为 34_android 设置着色器编译 set(SHADER_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments") set(SHADER_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/shaders") file(MAKE_DIRECTORY ${SHADER_OUTPUT_DIR}) # 将着色器源文件复制到构建目录 configure_file( "${SHADER_SOURCE_DIR}/27_shader_depth.frag" "${SHADER_OUTPUT_DIR}/27_shader_depth.frag" COPYONLY ) configure_file( "${SHADER_SOURCE_DIR}/27_shader_depth.vert" "${SHADER_OUTPUT_DIR}/27_shader_depth.vert" COPYONLY ) -
编译着色器:
# 编译着色器 set(SHADER_SOURCES "${SHADER_OUTPUT_DIR}/27_shader_depth.frag" "${SHADER_OUTPUT_DIR}/27_shader_depth.vert") add_shaders_target(android_shaders CHAPTER_NAME "${SHADER_OUTPUT_DIR}" SOURCES ${SHADER_SOURCES}) # 添加对着色器编译的依赖 add_dependencies(vulkan_tutorial_android android_shaders) -
在 Gradle 构建中引用编译的着色器:
sourceSets { main { assets { srcDirs = [ // 指向主项目的资源 '../../../../', // 附件目录中的模型和纹理 // 使用构建目录中本地编译的着色器 '.externalNativeBuild/cmake/debug/arm64-v8a/shaders', '.externalNativeBuild/cmake/debug/armeabi-v7a/shaders', // ... 其他 ABI ... ] } } }
跨平台加载资源
我们统一的 readFile 函数使得跨平台加载资源变得容易。以下是我们如何使用它来加载着色器文件:
// 使用跨平台函数加载着色器文件
#if PLATFORM_ANDROID
std::optional<AAssetManager*> optionalAssetManager = assetManager;
#else
std::optional<AAssetManager*> optionalAssetManager = std::nullopt;
#endif
std::vector<char> vertShaderCode = readFile("shaders/vert.spv", optionalAssetManager);
std::vector<char> fragShaderCode = readFile("shaders/frag.spv", optionalAssetManager);
我们使用相同的方法加载纹理图像和模型文件:
// 加载纹理图像
#if PLATFORM_ANDROID
std::optional<AAssetManager*> optionalAssetManager = assetManager;
std::vector<char> imageData = readFile(TEXTURE_PATH, optionalAssetManager);
// 处理图像数据...
#else
// 直接从文件系统加载
// ...
#endif
这种统一的方法让我们两全其美:我们为两个平台使用相同的代码结构,平台特定的差异由 readFile 函数本身处理。这使得我们的代码更易于维护和理解。
构建和运行
现在我们已经设置了包含所有必要组件的 Android 项目,让我们将所有内容整合起来,在 Android 设备上运行 Vulkan 应用。
过程很简单:
-
在 Android Studio 中打开项目。
-
连接 Android 设备或启动模拟器(确保它支持 Vulkan)。
-
点击 Android Studio 中的 "Run" 按钮。
Android Studio 将处理其余部分 - 它将构建应用,编译着色器,将所有内容打包到 APK 中,安装到设备/模拟器上并启动它。如果一切设置正确,您应该会看到 Vulkan 应用在 Android 上运行,渲染的场景与桌面版相同。
结论
在本章中,我们探讨了如何通过将 Vulkan 应用适配到 Android,将其从桌面带到移动端。我们已经看到,虽然核心 Vulkan API 在不同平台上保持一致,但周围的生态系统需要平台特定的适配。
我们的方法展示了几个关键原则,您可以将其应用到自己的 Vulkan 项目中:
-
代码重用:通过正确构建项目,我们可以为桌面和 Android 平台使用相同的核心渲染代码(34_android.cpp),最大限度地减少重复和维护开销。
-
现代 Android 集成:我们利用 Android Game SDK 中的 GameActivity 获得更好的性能和更简化的集成,而不是使用较旧的 NativeActivity 方法。
-
高效的资源管理:我们不是复制资源,而是从主项目中引用它们,确保一致性并减小 APK 大小。
-
本地着色器编译:通过在构建过程中编译着色器,我们早期捕获错误并确保跨平台的兼容性。
-
最少的平台特定代码:我们将平台特定的代码隔离在一个小型桥接文件中,保持核心 Vulkan 实现的整洁和可移植性。
这种方法不仅使应用更易于维护和更新,还为未来扩展到其他平台奠定了坚实的基础。当您改进核心渲染代码时,桌面和 Android 版本都会自动受益。
完整的 Android 示例可以在 attachments/android 目录中找到。您可以自由地将其用作自己 Android 上 Vulkan 项目的模板。
请记住,Vulkan HPP 默认不包含在 Android NDK 中,因此您需要从 Vulkan-Hpp GitHub 仓库 单独下载,或使用 Vulkan SDK 中包含的版本。