在这里插入图片描述

前言

在前一天的实战文章中,我们探讨了 NDK 配置、NAPI 基础注册以及字符串的跨语言处理。今天我们继续深入,走进 HarmonyOS NEXT 中一个极具技术含金量的领域——基于 XComponent + EGL + OpenGL ES 3.0 的完整图形渲染管线

为什么这个方向值得单独成篇?原因很简单:HarmonyOS NEXT 的声明式 UI 框架(ArkUI)固然强大,但在游戏渲染、视频滤镜、3D 可视化、实时特效等高性能图形场景下,GPU 加速的 Native 渲染是绕不开的技术拐点。而 XComponent 作为 ArkUI 与 Native 世界之间的桥梁,承担着将 C++ 渲染结果嵌入声明式页面的关键使命。

本文将完整覆盖以下内容:从 XComponent 的原理与注册,到 NativeWindow 的获取与 EGL 上下文初始化,再到 GLES 着色器编译、顶点缓冲区构建、渲染循环与帧同步,最后给出 ArkTS 侧的标准调用方式。整条链路环环相扣,代码全部基于 HarmonyOS NEXT / API 12+ 编写,可直接编译运行。


1. 架构总览:一条贯穿 ArkUI 与 GPU 的数据流

在深入代码之前,我们需要先理解整个渲染管线的数据流向。HarmonyOS NEXT 的图形架构可以分为三层:

第一层是 ArkTS/ArkUI 声明式界面层。开发者在这里编写 .ets 文件,用 XComponent 组件声明一块渲染画布,同时负责页面的布局、动画与用户交互。

第二层是 NAPI 胶水层。它用 C++ 实现了 ArkTS 与 Native 之间的跨语言调用约定。ArkTS 通过 NAPI 接口触发 C++ 中的渲染方法,传递 Surface ID、配置参数等信息。

第三层是 Native 渲染引擎层。这一层用 C++ 代码调用 EGL 和 GLES 3.0 API,从 XComponent 提供的 Surface(本质上是一个 NativeWindow)中获取 ANativeWindow 句柄,创建 EGL 上下文,上传顶点数据,执行着色器程序,最终将每一帧绘制到屏幕上。

整条链路可以用以下流程描述:ArkUI 创建 XComponent → 获取 Surface ID → 通过 NAPI 传递给 C++ → C++ 获取 ANativeWindow → 初始化 EGL/GLES → 进入渲染循环 → 每帧绘制 → ArkUI 控制生命周期(启动/停止/销毁)。

理解了这个宏观架构,接下来逐层实现就有了明确的方向感。


2. 项目结构与 CMake 构建配置

在动手写代码之前,先把项目架子搭好。一个完整的 Native 图形项目通常包含以下目录结构:

entry/src/main/
├── cpp/
│   ├── types/
│   │   └── libentry/
│   │       └── index.d.ts          # NAPI TypeScript 类型声明
│   ├── CMakeLists.txt              # CMake 构建脚本
│   ├── render_engine.cpp           # EGL/GLES 渲染引擎实现
│   ├── render_engine.h             # 渲染引擎头文件
│   ├── shader_utils.cpp            # 着色器编译辅助工具
│   └── napi_bridge.cpp             # NAPI 导出函数实现
└── ets/
    └── pages/
        └── index.ets               # ArkUI 调用侧代码

CMake 配置是整个 Native 模块能否编译成功的关键所在。以下是经过实际项目验证的完整 CMakeLists.txt,特别注意 GLESv3 和 EGL 库的链接方式:

# CMakeLists.txt
cmake_minimum_required(VERSION 3.18.1)

project("ogl_render_demo")

# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# OHOS NDK 路径由工具链自动注入,无需手动指定
# 查找 OpenHarmony NAPI 包
find_package(napi REQUIRED PATHS "${OHOS_NATIVE_DIR}/..")

# 查找 GLES 和 EGL
find_library(glesv3_lib GLESv3)
find_library(egl_lib EGL)

# 包含 NAPI 头文件目录
get_target_property(napi_include_dirs napi-sdk INTERFACE_INCLUDE_DIRECTORIES)

add_library(gl_render_demo SHARED
    napi_bridge.cpp
    render_engine.cpp
    shader_utils.cpp
)

target_include_directories(gl_render_demo PRIVATE
    ${napi_include_dirs}
    ${CMAKE_CURRENT_SOURCE_DIR}/types/libentry
)

target_link_libraries(gl_render_demo PRIVATE
    napi-sdk
    ${glesv3_lib}
    ${egl_lib}
    hilog_ndk.z
)

# 生成 .so 文件后自动安装到指定目录
install(TARGETS gl_render_demo LIBRARY DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/../resources/base/lib/)

小结一下:GLESv3 和 EGL 库是 Native 图形渲染的两块基石,缺一不可。在 CMake 中通过 find_library 查找后,必须链接到目标中,否则运行时会出现 “undefined symbol” 错误。


3. 渲染引擎头文件:定义核心接口

好的头文件设计是 C++ 代码可维护性的保障。我们的渲染引擎需要暴露以下几个核心能力:初始化(传入 Surface ID)、启动渲染循环、停止渲染循环、以及销毁资源。下面是头文件的完整定义:

// render_engine.h
#ifndef RENDER_ENGINE_H
#define RENDER_ENGINE_H

#include <cstdint>
#include <string>
#include <EGL/egl.h>
#include <GLES3/gl3.h>

class RenderEngine {
public:
    RenderEngine();
    ~RenderEngine();

    // 初始化渲染引擎,surfaceId 由 ArkTS 通过 NAPI 传入
    bool Initialize(int64_t surfaceId);

    // 启动渲染线程
    void StartRenderLoop();

    // 停止渲染线程
    void StopRenderLoop();

    // 主动触发一次渲染(由 NAPI 调用)
    void RenderFrame();

    // 销毁所有资源
    void Destroy();

    bool IsInitialized() const { return initialized_; }

private:
    // EGL 相关
    bool InitEGL(EGLNativeWindowType nativeWindow);
    bool CreateGLESContext();
    bool InitGLState();

    // 着色器相关
    GLuint CompileShader(GLenum type, const std::string& source);
    GLuint CreateShaderProgram(const std::string& vertexSrc, const std::string& fragmentSrc);
    void DestroyShaders();

    // 渲染逻辑
    void RenderTriangle();
    void RenderDynamicQuad();

    // 线程控制
    static void* RenderThreadFunc(void* arg);
    void RenderThreadMain();

    bool initialized_ = false;
    bool running_ = false;
    pthread_t render_thread_;
    pthread_mutex_t mutex_;

    // EGL 对象
    EGLDisplay egl_display_ = EGL_NO_DISPLAY;
    EGLContext egl_context_ = EGL_NO_CONTEXT;
    EGLSurface egl_surface_ = EGL_NO_SURFACE;
    EGLNativeWindowType native_window_ = nullptr;

    // GLES 对象
    GLuint shader_program_ = 0;
    GLuint vao_ = 0;
    GLuint vbo_ = 0;
    GLuint ebo_ = 0;

    // 渲染参数
    float time_offset_ = 0.0f;
    int frame_count_ = 0;
};

#endif // RENDER_ENGINE_H

这段头文件清晰地划分了 EGL 初始化、GLES 上下文管理、着色器编译、渲染逻辑以及线程管理五个职责区域。其中 pthread_mutex_t 的使用是为了保证渲染线程与 NAPI 调用线程之间的线程安全——这是本文方向 C 中会详细讨论的一个重要主题。


4. EGL 上下文初始化:连接窗口系统与 GLES

EGL 是 OpenGL ES 与底层窗口系统之间的抽象层。在 HarmonyOS NEXT 中,XComponent 提供的 Surface 经过 Native 适配后,以 ANativeWindow 的形式暴露给 Native 代码。EGL 的初始化流程本质上就是建立 Display(屏幕连接)、Config(像素格式配置)和 Surface(绘图表面)三者关系的过程。

// render_engine.cpp - EGL 初始化部分
#include "render_engine.h"
#include <hilog/log.h>
#include <window.h>

#undef LOG_TAG
#define LOG_TAG "GLRender"

#define CHECK_EGL_ERROR(msg) do { \
    EGLint err = eglGetError(); \
    if (err != EGL_SUCCESS) { \
        OH_LOG_ERROR(LOG_APP, "EGL Error: %{public}s, error=0x%x", msg, err); \
        return false; \
    } \
} while (0)

bool RenderEngine::InitEGL(EGLNativeWindowType nativeWindow) {
    // 第一步:获取默认 Display
    egl_display_ = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    if (egl_display_ == EGL_NO_DISPLAY) {
        OH_LOG_ERROR(LOG_APP, "Failed to get EGL display");
        return false;
    }

    // 第二步:初始化 EGL
    EGLint majorVersion = 0, minorVersion = 0;
    if (!eglInitialize(egl_display_, &majorVersion, &minorVersion)) {
        OH_LOG_ERROR(LOG_APP, "Failed to initialize EGL");
        return false;
    }
    OH_LOG_INFO(LOG_APP, "EGL version: %{public}d.%{public}d", majorVersion, minorVersion);

    // 第三步:选择配置
    EGLint configAttribs[] = {
        EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
        EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
        EGL_RED_SIZE, 8,
        EGL_GREEN_SIZE, 8,
        EGL_BLUE_SIZE, 8,
        EGL_ALPHA_SIZE, 8,
        EGL_DEPTH_SIZE, 16,
        EGL_NONE
    };

    EGLConfig config;
    EGLint numConfigs = 0;
    if (!eglChooseConfig(egl_display_, configAttribs, &config, 1, &numConfigs)) {
        CHECK_EGL_ERROR("eglChooseConfig");
        return false;
    }

    // 第四步:绑定 OpenGL ES API
    if (!eglBindAPI(EGL_OPENGL_ES_API)) {
        CHECK_EGL_ERROR("eglBindAPI");
        return false;
    }

    // 第五步:创建 Context(指定 GLES 3.0)
    EGLint contextAttribs[] = {
        EGL_CONTEXT_MAJOR_VERSION, 3,
        EGL_CONTEXT_MINOR_VERSION, 0,
        EGL_NONE
    };

    egl_context_ = eglCreateContext(egl_display_, config, EGL_NO_CONTEXT, contextAttribs);
    if (egl_context_ == EGL_NO_CONTEXT) {
        CHECK_EGL_ERROR("eglCreateContext");
        return false;
    }

    // 第六步:创建 Window Surface
    egl_surface_ = eglCreateWindowSurface(egl_display_, config, nativeWindow, nullptr);
    if (egl_surface_ == EGL_NO_SURFACE) {
        CHECK_EGL_ERROR("eglCreateWindowSurface");
        return false;
    }

    // 第七步:激活上下文
    if (!eglMakeCurrent(egl_display_, egl_surface_, egl_surface_, egl_context_)) {
        CHECK_EGL_ERROR("eglMakeCurrent");
        return false;
    }

    OH_LOG_INFO(LOG_APP, "EGL initialized successfully");
    return true;
}

上述代码中需要特别注意的是 eglBindAPI(EGL_OPENGL_ES_API) 这一步。在多 API 共存的系统中,显式绑定 API 可以避免歧义。此外,Context 的主版本号必须设为 3,才能使用 GLES 3.0 的核心特性(如 VAO、VBO 分离)。

阶段性小结:EGL 初始化的核心是"Display → Config → Context → Surface"四步曲。每一步都有对应的错误检查,任何一步失败都应该返回 false 并记录日志。


5. 着色器编译与程序链接:GLES 渲染的根基

GLES 3.0 抛弃了 Fixed Function Pipeline,完全依赖可编程管线。顶点着色器和片段着色器是两张并行的程序,由开发者用 GLSL ES 3.0 语法编写。以下代码展示了一个完整的着色器编译与链接过程:

// shader_utils.cpp
#include "render_engine.h"
#include <vector>

// 顶点着色器源码:接收位置和颜色属性
static const std::string VERTEX_SHADER_SRC = R"(
    #version 300 es
    precision mediump float;

    layout(location = 0) in vec3 aPosition;
    layout(location = 1) in vec4 aColor;

    uniform float uTime;
    uniform mat4 uMVP;

    out vec4 vColor;

    void main() {
        // 基于时间的正弦波动
        vec3 pos = aPosition;
        pos.y += sin(uTime * 2.0 + pos.x * 3.14159) * 0.1;

        gl_Position = uMVP * vec4(pos, 1.0);
        // 颜色随时间做余弦插值
        vColor = aColor * (0.8 + 0.2 * cos(uTime + pos.y));
    }
)";

// 片段着色器源码:插值颜色 + 边缘柔化
static const std::string FRAGMENT_SHADER_SRC = R"(
    #version 300 es
    precision mediump float;

    in vec4 vColor;
    out vec4 fragColor;

    void main() {
        fragColor = vColor;
    }
)";

GLuint RenderEngine::CompileShader(GLenum type, const std::string& source) {
    GLuint shader = glCreateShader(type);
    if (shader == 0) {
        OH_LOG_ERROR(LOG_APP, "glCreateShader failed");
        return 0;
    }

    const char* src = source.c_str();
    glShaderSource(shader, 1, &src, nullptr);
    glCompileShader(shader);

    GLint compiled = 0;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
    if (!compiled) {
        GLint infoLen = 0;
        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen);
        if (infoLen > 1) {
            std::vector<char> infoLog(infoLen);
            glGetShaderInfoLog(shader, infoLen, nullptr, infoLog.data());
            OH_LOG_ERROR(LOG_APP, "Shader compile error: %{public}s", infoLog.data());
        }
        glDeleteShader(shader);
        return 0;
    }

    OH_LOG_INFO(LOG_APP, "Shader compiled successfully (type=%{public}d)", type);
    return shader;
}

GLuint RenderEngine::CreateShaderProgram(const std::string& vertexSrc,
                                         const std::string& fragmentSrc) {
    GLuint vs = CompileShader(GL_VERTEX_SHADER, vertexSrc);
    GLuint fs = CompileShader(GL_FRAGMENT_SHADER, fragmentSrc);
    if (vs == 0 || fs == 0) {
        return 0;
    }

    GLuint program = glCreateProgram();
    glAttachShader(program, vs);
    glAttachShader(program, fs);

    // 注意:GLES 3.0 中无需手动绑定 location,layout 已经声明
    glLinkProgram(program);

    GLint linked = 0;
    glGetProgramiv(program, GL_LINK_STATUS, &linked);
    if (!linked) {
        GLint infoLen = 0;
        glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLen);
        if (infoLen > 1) {
            std::vector<char> infoLog(infoLen);
            glGetProgramInfoLog(program, infoLen, nullptr, infoLog.data());
            OH_LOG_ERROR(LOG_APP, "Program link error: %{public}s", infoLog.data());
        }
        glDeleteProgram(program);
        return 0;
    }

    // 链接后可删除着色器对象
    glDeleteShader(vs);
    glDeleteShader(fs);

    OH_LOG_INFO(LOG_APP, "Shader program linked successfully (ID=%{public}u)", program);
    return program;
}

在这段代码中,顶点着色器引入了两个 Uniform 变量:uTime 用于驱动动画时间轴,uMVP 用于将顶点从模型坐标变换到裁剪空间。着色器中的 layout(location = 0)layout(location = 1) 声明了顶点属性的精确位置,ArkTS 侧通过 NAPI 上传顶点数据时可以直接按位置索引写入,无需额外查询。


6. GL 状态初始化:VAO/VBO/EBO 构建

GLES 3.0 引入了 Vertex Array Object(VAO)这一核心概念。一个 VAO 封装了顶点属性的绑定状态,比 GLES 2.0 的分散式绑定更加高效且易于管理。以下代码展示了完整的缓冲区构建过程:

bool RenderEngine::InitGLState() {
    // 创建着色器程序
    shader_program_ = CreateShaderProgram(VERTEX_SHADER_SRC, FRAGMENT_SHADER_SRC);
    if (shader_program_ == 0) {
        return false;
    }
    glUseProgram(shader_program_);

    // 定义一个旋转四边形的顶点数据(位置 + RGBA 颜色)
    // 两个三角形组成矩形,顶点顺序:左下、右下、右上、左上
    float vertices[] = {
        // 位置 (x, y, z)          颜色 (r, g, b, a)
        -0.8f, -0.5f, 0.0f,       1.0f, 0.2f, 0.3f, 1.0f,   // 左下
         0.8f, -0.5f, 0.0f,       0.2f, 0.8f, 0.4f, 1.0f,   // 右下
         0.8f,  0.5f, 0.0f,       0.3f, 0.3f, 1.0f, 1.0f,   // 右上
        -0.8f,  0.5f, 0.0f,       1.0f, 0.9f, 0.2f, 1.0f,   // 左上
    };

    unsigned int indices[] = {
        0, 1, 2,   // 第一个三角形:左下→右下→右上
        0, 2, 3,   // 第二个三角形:左下→右上→左上
    };

    // 创建 VAO(GLES 3.0 核心)
    glGenVertexArrays(1, &vao_);
    glBindVertexArray(vao_);

    // 创建 VBO
    glGenBuffers(1, &vbo_);
    glBindBuffer(GL_ARRAY_BUFFER, vbo_);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    // 创建 EBO(Element Buffer Object,存储索引)
    glGenBuffers(1, &ebo_);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo_);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    // 启用顶点属性 0(位置)
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE,
                          7 * sizeof(float), (void*)0);

    // 启用顶点属性 1(颜色)
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE,
                          7 * sizeof(float), (void*)(3 * sizeof(float)));

    // 解绑
    glBindVertexArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

    OH_LOG_INFO(LOG_APP, "GL state initialized: VAO=%{public}u, VBO=%{public}u, EBO=%{public}u",
                vao_, vbo_, ebo_);
    return true;
}

VAO/VBO/EBO 三者的关系可以用一个生活化的比喻来理解:VAO 是"画板",它记录了数据的摆放方式;VBO 是"颜料盒",存储了顶点的实际坐标和颜色数据;EBO 是"索引卡片",告诉渲染器按什么顺序取用颜料。GLES 3.0 中,每个 draw call 都必须至少有一个 VAO,即使是最简单的场景也不能跳过。


7. 渲染循环:线程管理 + 帧同步

渲染循环的设计是整个图形引擎最核心的部分。它需要处理三个关键问题:第一,将渲染放在独立线程中执行,避免阻塞主线程;第二,使用正确的帧同步机制;第三,在生命周期结束时优雅退出。

void* RenderEngine::RenderThreadFunc(void* arg) {
    RenderEngine* engine = static_cast<RenderEngine*>(arg);
    engine->RenderThreadMain();
    return nullptr;
}

void RenderEngine::RenderThreadMain() {
    OH_LOG_INFO(LOG_APP, "Render thread started");

    // 确保 EGL 上下文在当前线程中激活
    if (!eglMakeCurrent(egl_display_, egl_surface_, egl_surface_, egl_context_)) {
        OH_LOG_ERROR(LOG_APP, "Failed to make EGL context current in render thread");
        return;
    }

    // 每帧时间统计(用于控制帧率)
    struct timespec last_time{};
    struct timespec current_time{};
    clock_gettime(CLOCK_MONOTONIC, &last_time);

    while (running_) {
        clock_gettime(CLOCK_MONOTONIC, &current_time);
        double deltaMs = (current_time.tv_sec - last_time.tv_sec) * 1000.0
                       + (current_time.tv_nsec - last_time.tv_nsec) / 1000000.0;
        last_time = current_time;

        time_offset_ += deltaMs / 1000.0f; // 转换为秒
        frame_count_++;

        // 执行 GL 渲染
        RenderDynamicQuad();

        // EGL 双缓冲交换(将后缓冲呈现到屏幕)
        eglSwapBuffers(egl_display_, egl_surface_);

        // 简单的帧率控制:约 60 FPS
        if (deltaMs < 16.67) {
            usleep(static_cast useconds_t>((16.67 - deltaMs) * 1000));
        }
    }

    eglMakeCurrent(egl_display_, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
    OH_LOG_INFO(LOG_APP, "Render thread exited");
}

void RenderEngine::RenderDynamicQuad() {
    // 清屏
    glClearColor(0.05f, 0.05f, 0.1f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glEnable(GL_DEPTH_TEST);

    glUseProgram(shader_program_);

    // 上传时间 Uniform
    GLint timeLoc = glGetUniformLocation(shader_program_, "uTime");
    glUniform1f(timeLoc, time_offset_);

    // 上传 MVP 矩阵(这里简化为单位矩阵,实际项目应接入 3D 数学库)
    GLint mvpLoc = glGetUniformLocation(shader_program_, "uMVP");
    float mvp[16] = {
        1, 0, 0, 0,
        0, 1, 0, 0,
        0, 0, 1, 0,
        0, 0, 0, 1
    };
    glUniformMatrix4fv(mvpLoc, 1, GL_FALSE, mvp);

    // 绑定 VAO 并绘制
    glBindVertexArray(vao_);
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
    glBindVertexArray(0);

    // 每 60 帧打印一次 FPS(便于调试)
    if (frame_count_ % 60 == 0) {
        OH_LOG_INFO(LOG_APP, "Frame count: %{public}d, Time: %{public}.2f s",
                    frame_count_, time_offset_);
    }
}

这段代码中有几个值得深入探讨的设计细节。首先,渲染线程启动时调用 eglMakeCurrent 是因为 EGL 上下文绑定是线程绑定的,主线程初始化的上下文无法直接在其他线程中使用。其次,eglSwapBuffers 实现了双缓冲机制——渲染指令写入后缓冲,交换操作将前后缓冲互换,保证每帧显示的是完整的画面,避免撕裂现象。

关键提示:GLES 的渲染命令是异步的,调用 glDrawArraysglDrawElements 只是将命令提交给 GPU,GPU 何时完成渲染并不影响 CPU 继续执行后续代码。但 eglSwapBuffers 是同步的,它会阻塞直到实际交换完成。


8. NAPI 胶水层:ArkTS 与 C++ 的双向通道

NAPI 是 Node.js API 的跨平台实现,也是 HarmonyOS NEXT 中 ArkTS 与 Native 代码通信的标准方式。我们的渲染引擎通过以下 NAPI 接口暴露给 ArkTS 层:

// napi_bridge.cpp
#include <napi/native_api.h>
#include <hilog/log.h>
#include "render_engine.h"
#include <memory>

#undef LOG_TAG
#define LOG_TAG "NapiBridge"

// 全局引擎实例(实际项目中建议用 napi_ref 持久化管理)
static RenderEngine* g_renderEngine = nullptr;

static napi_value StartRender(napi_env env, napi_callback_info info) {
    size_t argc = 1;
    napi_value args[1];
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    int64_t surfaceId = 0;
    if (argc >= 1) {
        napi_get_value_int64(env, args[0], &surfaceId);
    }

    OH_LOG_INFO(LOG_APP, "NAPI: StartRender with surfaceId=%{public}lld", surfaceId);

    if (g_renderEngine == nullptr) {
        g_renderEngine = new RenderEngine();
    }

    if (!g_renderEngine->IsInitialized()) {
        if (!g_renderEngine->Initialize(surfaceId)) {
            OH_LOG_ERROR(LOG_APP, "NAPI: Initialize failed");
            napi_throw(env, napi_create_string_utf8(env, "Initialize failed",
                        NAPI_AUTO_LENGTH, nullptr));
            return nullptr;
        }
    }

    g_renderEngine->StartRenderLoop();

    return nullptr;
}

static napi_value StopRender(napi_env env, napi_callback_info info) {
    OH_LOG_INFO(LOG_APP, "NAPI: StopRender");
    if (g_renderEngine != nullptr) {
        g_renderEngine->StopRenderLoop();
    }
    return nullptr;
}

static napi_value DestroyEngine(napi_env env, napi_callback_info info) {
    OH_LOG_INFO(LOG_APP, "NAPI: DestroyEngine");
    if (g_renderEngine != nullptr) {
        g_renderEngine->Destroy();
        delete g_renderEngine;
        g_renderEngine = nullptr;
    }
    return nullptr;
}

// NAPI 模块导出描述
static napi_value Export(napi_env env, napi_value exports) {
    napi_property_descriptor desc[] = {
        {"startRender", nullptr, StartRender, nullptr, nullptr, nullptr,
         napi_default, nullptr},
        {"stopRender", nullptr, StopRender, nullptr, nullptr, nullptr,
         napi_default, nullptr},
        {"destroy", nullptr, DestroyEngine, nullptr, nullptr, nullptr,
         napi_default, nullptr},
    };

    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    return exports;
}

// 模块注册入口:HarmonyOS NEXT 使用 NAPI_MODULE_INIT
NAPI_MODULE_INIT() {
    OH_LOG_INFO(LOG_APP, "NAPI module gl_render_demo registered");
    return Export(env, exports);
}

NAPI_MODULE_INIT 是 HarmonyOS NEXT / OpenHarmony 的标准模块注册宏,它会自动在 .so 加载时执行 Export 函数,将三个导出的方法绑定到 exports 对象上。ArkTS 侧通过 import native from 'libgl_render_demo.so' 即可拿到这些方法。

有一点需要特别注意:上述代码中 g_renderEngine 作为全局指针仅用于演示目的。在生产环境中,建议使用 napi_create_reference 持久化引擎对象,并通过 napi_add_env_cleanup_hook 在 ArkTS 对象被垃圾回收时自动触发 C++ 资源的释放,以避免 Native 内存泄漏。


9. TypeScript 类型声明:让 IDE 智能提示生效

NAPI 模块的 .d.ts 声明文件是 ArkTS 与 Native 之间类型安全的保障。以下是完整的声明文件:

// types/libentry/index.d.ts
/**
 * OpenGL ES 渲染模块
 * 用于通过 Native C++ 在 XComponent 上进行 GPU 加速渲染
 */
export const startRender: (surfaceId: number) => void;
export const stopRender: () => void;
export const destroy: () => void;

/**
 * 获取 Native 引擎当前状态
 * @returns true 表示引擎已初始化并运行
 */
export function isEngineReady(): boolean;

声明文件的路径和命名必须与 CMake 中配置的输出目录一致,否则 hdc 编译时无法将类型信息与 .so 关联起来。


10. ArkTS 调用侧:XComponent 与渲染管线的连接

终于到了最后一环——ArkUI 页面代码。ArkTS 侧的核心任务是创建 XComponent 组件、获取其 Surface ID,然后调用 Native 模块的启动方法:

// ets/pages/index.ets
import native from 'libgl_render_demo.so'

@Entry
@Component
struct Index {
  // XComponent 控制器,用于获取 Surface ID
  private xcController: XComponentController = new XComponentController()
  @State message: string = 'GL Render Demo'
  @State isRendering: boolean = false

  build() {
    Column() {
      Text(this.message)
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20, bottom: 20 })

      // XComponent:声明一块 Native 渲染画布
      XComponent({
        id: 'gl_render_xc',
        type: XComponentType.SURFACE,   // SURFACE 类型才能接收 NativeWindow
        controller: this.xcController
      })
        .width('100%')
        .height('70%')
        .backgroundColor(Color.Black)
        .onLoad(() => {
          // 组件加载完成时获取 Surface ID
          const surfaceId = this.xcController.getXComponentSurfaceId()
          console.info(`XComponent loaded, surfaceId: ${surfaceId}`)

          // 启动 Native 渲染管线
          native.startRender(surfaceId)
          this.isRendering = true
          this.message = 'Rendering... (tap to stop)'
        })

      // 控制按钮区
      Row() {
        Button('Start')
          .enabled(!this.isRendering)
          .onClick(() => {
            const surfaceId = this.xcController.getXComponentSurfaceId()
            native.startRender(surfaceId)
            this.isRendering = true
            this.message = 'Rendering...'
          })

        Button('Stop')
          .enabled(this.isRendering)
          .margin({ left: 20 })
          .onClick(() => {
            native.stopRender()
            this.isRendering = false
            this.message = 'Stopped. Tap Start to resume.'
          })

        Button('Destroy')
          .margin({ left: 20 })
          .onClick(() => {
            native.destroy()
            this.isRendering = false
            this.message = 'Engine destroyed.'
          })
      }
      .padding({ bottom: 40 })
      .justifyContent(FlexAlign.Center)
      .width('100%')
    }
    .width('100%')
    .height('100%')
  }

  // 页面退出时自动销毁资源
  aboutToDisappear() {
    native.destroy()
  }
}

在这里插入图片描述

XComponentType.SURFACE 是整个链路中的关键枚举值。与 CANVAS 类型不同,SURFACE 类型的 XComponent 会创建一个 NativeWindow 句柄,Native 代码可以通过 Surface ID 拿到这个句柄并初始化 EGL。没有这一步,EGL 就无法将渲染结果输出到正确的显示区域。

此外,aboutToDisappear 生命周期钩子的处理不可省略。Flutter 和 React Native 中也有类似的机制——页面销毁时必须同步释放 Native 资源,否则会导致野指针问题,轻则画面卡死,重则触发 Native Crash。


11. 渲染效果解析:顶点动画的数学原理

文章至此已经给出了完整的渲染管线代码,有必要再回头解析一下顶点和片段着色器中的数学逻辑,这有助于读者举一反三地改造自己的渲染效果。

顶点着色器中的这条语句:

pos.y += sin(uTime * 2.0 + pos.x * 3.14159) * 0.1;

实现了一个水平方向传播的正弦波。uTime 随每帧递增,使得波峰和波谷沿 X 轴方向平移,产生流动感。pos.x * 3.14159 将空间坐标映射到弧度域,保证不同 X 位置的顶点有不同的相位偏移。乘以 0.1 是振幅控制,防止变形过于剧烈。

片段着色器中的颜色插值:

vColor = aColor * (0.8 + 0.2 * cos(uTime + pos.y));

引入了第二个时间驱动函数 cos,它与正弦波互补,使得颜色变化的节奏与形状的起伏不完全同步,从而产生有机的视觉节奏感。

这两行代码虽然简短,但展示了一个完整的"时间驱动动画"模式——这是游戏引擎和实时特效中最高频使用的技术范式。


12. 实战注意事项与常见坑点

将上述代码组合成一个可运行的项目还需要注意以下几个高频踩坑点:

第一,NativeWindow 的生命周期问题。 Surface ID 只在 XComponent 加载完成后才有效。如果在 onLoad 回调之前就调用 startRender,Native 代码拿到的将是一个无效的窗口句柄。

第二,EGL 上下文丢失与恢复。 在多任务切换或息屏场景下,EGL Surface 可能被系统销毁。此时 eglSwapBuffers 会返回 EGL_CONTEXT_LOST。渲染线程必须检测此错误并重新初始化上下文和 GL 资源。完整的实现需要维护一个"资源状态机"。

第三,跨线程 NAPI 调用。 渲染线程中不应直接调用 napi_* 函数,因为 NAPI 环境绑定在发起调用的 ArkTS 线程上。如果需要在渲染线程中触发 ArkTS 回调,应使用 napi_create_threadsafe_function 机制。

第四,CMake 中的 ABI 过滤。 确保 .ArkMultiGrid 配置文件中指定了正确的 ABI 类型(通常为 armeabi-v7aarm64-v8a),否则 CMake 可能使用错误的工具链编译出无法在目标设备上运行的 .so 文件。

第五,日志标签注册。 使用 OH_LOG_INFOOH_LOG_ERROR 时,LOG_APP 宏定义了一个日志领域(domain)。这个领域需要在 AppScope/app.json5 中正确注册,否则日志可能不会输出到 hilog 中。


结语

本文从 XComponent 的 Surface 获取出发,一路延伸到 EGL 上下文创建、GLES 3.0 着色器管线、VAO/VBO/EBO 缓冲区构建、pthread 渲染线程管理,最终回到 ArkTS 侧的完整调用示例。这条链路覆盖了 HarmonyOS NEXT 图形渲染开发中最核心、最实用的技术要素。

通过这个实战框架,开发者可以在 XComponent 上自由地扩展:加载模型文件实现 3D 渲染、接入物理引擎做粒子特效、对接视频解码器实现视频滤镜。GLES 3.0 提供的可编程管线为这些场景提供了足够强的底层能力,而 NAPI 则保证了这些能力可以被上层的声明式 UI 无缝调用。

掌握了这套"桥接模式",ArkUI 的声明式优雅与 Native 的性能极致就可以兼得了。


Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐