HarmonyOS 时域 AI 超分:用 Vulkan 让多帧融合变清晰

什么是时域 AI 超分

前面我们介绍了空域超分和空域 AI 超分,它们都是基于单帧图像来放大。但还有一种更厉害的超分方式:时域 AI 超分。它不仅看当前帧,还会利用前面几帧的信息来重建高清图像。

打个比方:空域超分就像一个画家看着一张小照片画大画;时域超分则像一个画家同时看好几张角度略有不同的小照片,然后综合所有信息画出一张更清晰的大画。因为有多帧信息,细节会更丰富,抗锯齿效果也更好。

时域超分的一个关键概念是"相机抖动"(Jitter)。每帧渲染时,相机的采样位置会微微偏移一点,这样连续几帧就能采集到不同位置的像素信息。这些信息融合起来,就相当于用更高的采样率来渲染,画质自然更好。

环境搭建

硬件要求

  • 设备类型:请参考 XEngine 开发指南的硬件要求

软件要求

  • DevEco Studio 版本:DevEco Studio 6.0.0 Release 及以上
  • HarmonyOS SDK 版本:HarmonyOS 6.0.0 Release SDK 及以上

搭建步骤

  1. 安装 DevEco Studio:去华为开发者官网下载安装
  2. 配置开发环境:确保网络环境正常
  3. 设备调试:使用真机进行调试

项目结构

├── entry/src/main                   // 代码区
│  ├── cpp
│  │  ├── types
│  │  │  └── libnativerender
│  │  │     └── index.d.ts       // native层接口注册文件
│  │  ├── napi_init.cpp           // native api层接口的具体实现函数
│  │  ├── CMakeLists.txt         // native层编译配置
│  │  ├── 3rdParty              // 三方件
│  │  ├── common              // 通用接口
│  │  ├── file                   // 文件管理
│  │  ├── libs                   // 三方动态库
│  │  ├── manager               // native&arkts交互
│  │  ├── render                 // 渲染
│  │  └── vulkanbase             // vulkan基础能力封装
│  └── ets
│     ├── entryability
│     │  └── EntryAbility.ts       // 程序入口类
│     └── pages
│        └── index.ets            // 主界面展示类
└── resources                     // 资源文件目录
   ├── base
   │  └── media
   │     └── icon.png           // 图片资源
   └── rawfile
      └── Sponza
         └── sponza.obj         // 模型资源

第一步:引入头文件

#include <algorithm>
#include <string>
#include <vector>
#include "xengine/xeg_vulkan_temporal_upscale.h"
#include "xengine/xeg_vulkan_extension.h"

和空域 AI 超分类似,但用的是时域超分的头文件 xeg_vulkan_temporal_upscale.h

第二步:配置 CMakeLists.txt

find_library(
    xengine-lib
    xengine
)

target_link_libraries(nativerender PUBLIC
    ${hilog-lib} ${libace-lib} ${libnapi-lib} ${libuv-lib} libnative_window.so libc++.a libktx librawfile.z.so libassimp ${xengine-lib})

这里链接了 xengine 库以及其他必要的依赖库。

第三步:查询设备是否支持

VkPhysicalDevice physicalDevice;
std::vector<std::string> supportedExtensions;
uint32_t pPropertyCount;
HMS_XEG_EnumerateDeviceExtensionProperties(physicalDevice, &pPropertyCount, nullptr);
if (pPropertyCount > 0) {
    std::vector<XEG_ExtensionProperties> pProperties(pPropertyCount);
    if (HMS_XEG_EnumerateDeviceExtensionProperties(physicalDevice, &pPropertyCount,
        &pProperties.front()) == VK_SUCCESS) {
        for (auto ext : pProperties) {
            supportedExtensions.push_back(ext.extensionName);
        }
    }
}
if (std::find(supportedExtensions.begin(), supportedExtensions.end(), XEG_TEMPORAL_UPSCALE_EXTENSION_NAME) ==
    supportedExtensions.end()) {
    exit(1); //return error;
}

和其他 Vulkan 扩展一样,先查询设备支持的扩展列表,然后检查是否支持时域 AI 超分。注意这里的扩展名是 XEG_TEMPORAL_UPSCALE_EXTENSION_NAME

第四步:创建时域 AI 超分实例

XEG_TemporalUpscale xegTemporalUpscale;

声明一个时域 AI 超分实例句柄。

uint32_t highResWidth = 800;
uint32_t highResHeight = 600;
uint32_t lowResWidth = 1200;
uint32_t lowResHeight = 900;
const uint32_t jitterNum = 8;
VkDevice device;

定义一些参数:

  • highResWidthhighResHeight:输入图像的尺寸(低分辨率渲染的结果)
  • lowResWidthlowResHeight:输出图像的尺寸(超分后的高分辨率)
  • jitterNum:相机抖动的周期数,表示每 8 帧为一个循环
  • device:Vulkan 逻辑设备
XEG_TemporalUpscaleCreateInfo createInfo;
createInfo.inputSize = {highResWidth, highResHeight};
createInfo.outputSize = {lowResWidth, lowResHeight};
createInfo.outputFormat = VK_FORMAT_R8G8B8A8_UNORM;
createInfo.jitterNum = jitterNum;
createInfo.isDepthReversed = true;

配置创建参数:

  • inputSize:输入图像的尺寸
  • outputSize:输出图像的尺寸
  • outputFormat:输出图像的颜色格式,VK_FORMAT_R8G8B8A8_UNORM 表示 RGBA 四通道,每个通道 8 位
  • jitterNum:相机抖动的周期数
  • isDepthReversed:深度值是否反转。有些渲染管线的深度值是反转的(近处值大,远处值小),设为 true 表示需要反转
VkResult res = HMS_XEG_CreateTemporalUpscale(device, &createInfo, &xegTemporalUpscale);
if (res != VK_SUCCESS) {
    exit(1); //return error;
}

调用 HMS_XEG_CreateTemporalUpscale 创建实例。如果创建失败,退出程序。

第五步:计算相机抖动(Jitter)

时域超分的核心之一是相机抖动。每帧渲染时,相机的采样位置会微微偏移,这样连续几帧就能采集到不同位置的像素信息。

Halton 序列

Halton 序列是一种低差异序列,生成的点分布很均匀。我们用它来计算每帧的抖动量。

float GetHaltonSequence(uint32_t index, uint32_t base) {
    float result = 0.0;
    float fraction = 1.0 / base;
    while (index > 0) {
        result += fraction * (index % base);
        index /= base;
        fraction /= base;
    }
    return result;
}

这个函数生成 Halton 序列的第 index 个值,base 是基数。用 base=2 生成 X 方向的抖动,用 base=3 生成 Y 方向的抖动。

计算每帧的 Jitter 值

uint64_t frameNum = 0;
float jitterX = 0.0;
float jitterY = 0.0;

frameNum 是当前帧数,需要每帧加 1。jitterXjitterY 是当前帧的抖动量。

jitterX = GetHaltonSequence((frameNum % jitterNum) + 1, 2) - 0.5;
jitterY = GetHaltonSequence((frameNum % jitterNum) + 1, 3) - 0.5;

先用 Halton 序列生成 [0, 1] 范围的值,然后减去 0.5,变成 [-0.5, 0.5] 范围。

jitterX = jitterX / highResWidth;
jitterY = jitterY / highResHeight;

再除以输入图像的分辨率,得到 UV 坐标下的 Jitter 值。这样抖动量就和图像尺寸成比例了。

Jitter 计算的整体流程如下:

获取当前帧号 frameNum

计算帧号对 jitterNum 取模

用 Halton 序列 base=2 生成 X 方向值

用 Halton 序列 base=3 生成 Y 方向值

减去 0.5 映射到 -0.5, 0.5

除以输入分辨率得到 UV 坐标偏移

取负号作为最终 Jitter 值

第六步:执行时域 AI 超分

XEG_TemporalUpscaleDescription xegDescription;
VkImageView inputImageView = VK_NULL_HANDLE;
VkImageView motionVectorImageView = VK_NULL_HANDLE;
VkImageView depthImageView = VK_NULL_HANDLE;
VkImageView dynamicMaskImageView = VK_NULL_HANDLE;
VkImageView outputImageView = VK_NULL_HANDLE;
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;

定义超分描述结构体和各种图像视图:

  • inputImageView:输入图像(低分辨率渲染结果)
  • depthImageView:深度图像,记录每个像素离相机有多远
  • motionVectorImageView:运动矢量图像,记录每个像素的运动方向和距离
  • dynamicMaskImageView:动态遮罩图像,标记哪些物体是运动的
  • outputImageView:输出图像(超分后的高分辨率结果)
xegDescription.inputImage = inputImageView;
xegDescription.depthImage = depthImageView;
xegDescription.motionVectorImage = motionVectorImageView;
xegDescription.dynamicMaskImage = dynamicMaskImageView;
xegDescription.outputImage = outputImageView;

把各种图像设置到描述结构体中。

xegDescription.jitterX = -jitterX;
xegDescription.jitterY = -jitterY;

设置抖动值。注意这里取了负号,因为抖动是应用在采样坐标上的,方向相反。

xegDescription.resetHistory = (frameNum == 0) ? true : false;

设置是否重置历史帧数据。第一帧时设为 true,因为没有历史数据可用。之后的帧设为 false,让引擎使用历史数据来提升画质。

xegDescription.steadyLevel = 0.5;

设置画面偏向当前帧还是历史帧的平衡程度。取值范围 [0.0, 1.0]:

  • 值越小,越偏向当前帧,画面更"新鲜",但可能有噪点
  • 值越大,越偏向历史帧,画面更稳定,但可能有拖影
  • 0.5 是一个平衡值
HMS_XEG_CmdRenderTemporalUpscale(commandBuffer, xegTemporalUpscale, &xegDescription);

最后调用 HMS_XEG_CmdRenderTemporalUpscale 执行时域 AI 超分。这个命令会被记录到命令缓冲区里,等 GPU 执行时才会真正运行。

时域 AI 超分的执行过程涉及多个输入数据的协同处理:

输入图像 低分辨率渲染结果

HMS_XEG_CmdRenderTemporalUpscale

深度图像 像素到相机距离

运动矢量图像 像素运动方向与距离

动态遮罩图像 标记运动物体

Jitter 值 相机抖动偏移

输出图像 高分辨率超分结果

历史帧数据

steadyLevel 平衡参数

第七步:销毁实例

if (xegTemporalUpscale) {
    HMS_XEG_DestroyTemporalUpscale(xegTemporalUpscale);
}

不需要时域 AI 超分功能时,销毁实例,释放内存资源。

时域 AI 超分的完整工作流程如下:

查询设备是否支持时域AI超分

创建时域AI超分实例

配置输入输出尺寸和参数

每帧计算 Jitter 值

准备输入数据

设置 Jitter 和稳态参数

是否为首帧?

重置历史帧数据

使用历史帧融合

执行时域AI超分

输出高分辨率图像

时域超分和空域超分的区别

你可能会问:时域超分和空域超分有什么区别?

方面 时域超分 空域超分
输入数据 多帧图像 + 深度 + 运动矢量 单帧图像
抗锯齿 有,效果很好 没有
画质 更好,细节更丰富 一般
性能开销 更大 较小
实现复杂度 更高,需要管理历史帧 较低

简单来说:时域超分效果更好,但更复杂、开销更大。如果你的应用对画质要求很高,而且能承受额外的性能开销,时域超分是更好的选择。

适用场景

时域 AI 超分特别适合以下场景:

  • 3D 游戏:画面复杂,有很多运动物体,时域超分能有效抗锯齿
  • VR/AR 应用:对画质和帧率都有很高要求
  • 视频渲染:连续帧之间有很强的时域相关性,超分效果最好

注意事项

  1. 设备支持:不是所有设备都支持时域 AI 超分,一定要先查询扩展
  2. Jitter 采样范围:Jitter 的分布范围要足够大,才能有效减轻锯齿
  3. 运动矢量:运动矢量要准确,否则超分结果会有重影
  4. 历史帧管理:第一帧要重置历史数据,否则会有错误的融合结果
  5. steadyLevel 调节:要根据画面运动程度来调整,运动剧烈时要偏向当前帧
  6. 深度值方向:注意 isDepthReversed 的设置,不同渲染管线的深度值方向可能不同

总结

时域 AI 超分是一个更高级的超分技术,它利用多帧信息来重建高清图像。核心流程:

  1. 查询设备是否支持时域 AI 超分
  2. 创建时域 AI 超分实例,配置参数
  3. 每帧计算 Jitter 值(使用 Halton 序列)
  4. 准备输入数据(颜色图、深度图、运动矢量、动态遮罩)
  5. 调用 HMS_XEG_CmdRenderTemporalUpscale 执行超分
  6. 不需要时销毁实例

如果你对画质有很高要求,时域 AI 超分是一个值得尝试的技术。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐