前面文章《HarmonyOS 6 自定义人脸识别模型2:OH_NativeXComponent方式绘制》介绍了如何将ArkTS层的XComponent与C++层的OH_NativeXComponent进行关联与映射,文本接着介绍如何在C++中通过OpenGL在OH_NativeXComponent中进行绘制等操作。

OpenGL介绍

OpenGL (Open Graphics Library) 是一个跨编程语言、跨平台的编程图形接口,用于渲染2D、3D矢量图形。在移动设备开发中,我们通常使用的是 OpenGL ES (OpenGL for Embedded Systems),它是 OpenGL 的子集,去除了冗余功能,专门为嵌入式系统设计。

在 HarmonyOS 中,我们使用 OpenGL ES 来进行高性能的图形渲染。而要让 OpenGL ES 工作,还需要 EGL (Embedded-System Graphics Library)。EGL 是 Khronos 渲染 API(如 OpenGL ES)与底层原生窗口系统之间的接口。它负责:

  • 管理图形渲染管线。
  • 创建渲染表面(Surface)。
  • 管理渲染上下文(Context)。
    简单来说,EGL 是 OpenGL ES 与屏幕(Window)之间的“胶水”
HarmonyOS 中OpenGL操作流程

在 HarmonyOS NDK 开发中,使用 OpenGL 进行绘制通常遵循以下标准流程:

  1. 获取原生窗口句柄:通过 OH_NativeXComponent 获取底层的 NativeWindow
  2. 创建 EGL Display:建立与本地窗口系统的连接。
  3. 初始化 EGL:设置 EGL 的版本信息。
  4. 选择 EGL Config:配置渲染参数(如颜色位宽、采样率等)。
  5. 创建 EGL Surface:将 NativeWindow 绑定到 EGL。
  6. 创建 EGL Context:创建渲染状态机。
  7. 绑定上下文(Make Current):将当前线程与 EGL 上下文绑定。
  8. 执行 OpenGL 渲染指令:使用渲染程序(Shader Program)进行绘图。
  9. 交换缓冲区(Swap Buffers):将渲染内容显示到屏幕上。
OpenGL基于OH_NativeXComponent绘制

接下来详细介绍如何按照上面步骤实现具体的绘制流程,这里我们把主要逻辑封装在 EGLCorePluginRender 类中。

1. 初始化 EGL 环境

OnSurfaceCreatedCB 回调中,我们获取到 NativeWindow 并触发 EglContextInit

// egl_core.cpp
bool EGLCore::EglContextInit(void* window, int width, int height)
{
    UpdateSize(width, height);
    eglWindow_ = reinterpret_cast<EGLNativeWindowType>(window);

    // 1. 初始化 display
    eglDisplay_ = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    
    // 2. 初始化 EGL
    EGLint majorVersion;
    EGLint minorVersion;
    if (!eglInitialize(eglDisplay_, &majorVersion, &minorVersion)) {
        return false;
    }

    // 3. 选择配置
    const EGLint maxConfigSize = 1;
    EGLint numConfigs;
    if (!eglChooseConfig(eglDisplay_, ATTRIB_LIST, &eglConfig_, maxConfigSize, &numConfigs)) {
        return false;
    }
    
    // 4. 创建环境(Surface 和 Context)
    return CreateEnvironment();
}

bool EGLCore::CreateEnvironment()
{
    // 创建 Surface
    eglSurface_ = eglCreateWindowSurface(eglDisplay_, eglConfig_, eglWindow_, NULL);
    
    // 创建 Context
    eglContext_ = eglCreateContext(eglDisplay_, eglConfig_, EGL_NO_CONTEXT, CONTEXT_ATTRIBS);
    
    // 绑定当前线程
    if (!eglMakeCurrent(eglDisplay_, eglSurface_, eglSurface_, eglContext_)) {
        return false;
    }
    
    // 创建着色器程序 (Program)
    program_ = CreateProgram(VERTEX_SHADER, FRAGMENT_SHADER);
    return true;
}
2. 执行渲染逻辑

渲染时,我们需要通过 glUseProgram 激活程序,并向顶点着色器传递顶点坐标和颜色数据。

// egl_core.cpp
void EGLCore::Draw(int& hasDraw)
{
    GLint position = PrepareDraw(); // 调用 glUseProgram, glViewport 等
    
    // 绘制背景颜色
    ExecuteDraw(position, BACKGROUND_COLOR, BACKGROUND_RECTANGLE_VERTICES, sizeof(BACKGROUND_RECTANGLE_VERTICES));

    // 计算五角星顶点并绘制
    // ... (具体顶点计算逻辑详见源代码)
    ExecuteDrawStar(position, DRAW_COLOR, shapeVertices, sizeof(shapeVertices));

    // 结束绘制并刷新缓冲区
    FinishDraw();
    hasDraw = 1;
}

bool EGLCore::FinishDraw()
{
    glFlush();
    glFinish();
    // 将绘制内容“展示”到屏幕上
    return eglSwapBuffers(eglDisplay_, eglSurface_);
}
3. 关联 NativeXComponent

上面EGL相关流程和系统系统很类似,具体到Window中的绑定这里重点介绍下,在获取到NativeXcomponent后给NativeXComponent注册各种回调,包括渲染回调:

void PluginRender::RegisterCallback(OH_NativeXComponent *nativeXComponent) {  
  memset(&renderCallback_, 0, sizeof(OH_NativeXComponent_Callback));  
  renderCallback_.OnSurfaceCreated = OnSurfaceCreatedCB;  
  renderCallback_.OnSurfaceChanged = OnSurfaceChangedCB;  
  renderCallback_.OnSurfaceDestroyed = OnSurfaceDestroyedCB;  
  renderCallback_.DispatchTouchEvent = DispatchTouchEventCB;  
  OH_NativeXComponent_RegisterCallback(nativeXComponent, &renderCallback_);  
}

其中OnSurfaceCreatedCB回调中将这些 OpenGL 操作与 OH_NativeXComponent 的生命周期结合起来。当 Surface 创建、大小改变或通过 NAPI 被触发时,调用 EGLCore 相应的方法。OnSurfaceCreatedCB回调中包括了一个window参数,window 通过eglWindow_ = reinterpret_cast<EGLNativeWindowType>(window);转换为EGLNativeWindowType可以用来初始化EGL上下文:

// plugin_render.cpp
void OnSurfaceCreatedCB(OH_NativeXComponent* component, void* window)
{
    // ... 获取 id 和 size ...
    if (render->eglCore_->EglContextInit(window, width, height)) {
        render->eglCore_->Background(); // 初始背景色渲染
    }
}

// 供 ArkTS 层调用的绘制方法
napi_value PluginRender::NapiDrawPattern(napi_env env, napi_callback_info info)
{
    // ... 获取 PluginRender 实例 ...
    render->eglCore_->Draw(hasDraw_);
    return nullptr;
}

OnSurfaceChangedCB回调触发画布大小更新,用来更新EGL大小:

void PluginRender::OnSurfaceChanged(OH_NativeXComponent *component,  
                                    void *window) {  
  char idStr[OH_XCOMPONENT_ID_LEN_MAX + 1] = {'\0'};  
  uint64_t idSize = OH_XCOMPONENT_ID_LEN_MAX + 1;  
  if (OH_NativeXComponent_GetXComponentId(component, idStr, &idSize) !=  
      OH_NATIVEXCOMPONENT_RESULT_SUCCESS) {  
    OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "Callback",  
                 "OnSurfaceChanged: Unable to get XComponent id");  
    return;  
  }  
  
  std::string id(idStr);  
  PluginRender *render = PluginRender::GetInstance(id);  
  uint64_t width;  
  uint64_t height;  
  OH_NativeXComponent_GetXComponentSize(component, window, &width, &height);  
  if (render != nullptr) {  
    render->eglCore_->UpdateSize(width, height);  
  }  
}

此外还有OnSurfaceDestroyed画布销毁以及DispatchTouchEvent事件触发等回调。

OpenGL绘制五角星

本文以官方五角星绘制为例,五角星的绘制采用了一种更具技巧性的几何分解方法,而不是直接定义十个顶点:

(1)几何分解

我们将五角星分解为 5 个完全相同的四边形(筝形)
在这里插入图片描述

每个四边形由以下四个关键点组成:

  • 中心点 (Center):五角星的几何中心。
  • 外顶点 (Tip):五角星的五个尖角之一。
  • 两个内顶点 (Shoulders):连接外顶点与中心点的凹角处。

这种分解方式的优势在于,我们只需要关注其中一个“角”的几何构造,其余部分完全可以通过数学变换(旋转)得到。

(2)数学旋转与坐标变换

为了简化计算,我们首先计算出其中一个四边形的 4 个顶点坐标。然后利用旋转矩阵,将其绕中心点旋转 72 度(即 2π/52\pi/52π/5 弧度),重复 4 次,即可得到完整的五角星。

相关的二维旋转逻辑封装在 Rotate2d 函数中:

void EGLCore::Rotate2d(GLfloat centerX, GLfloat centerY, GLfloat *rotateX, GLfloat *rotateY, GLfloat theta) {
  GLfloat tempX = cos(theta) * (*rotateX - centerX) - sin(theta) * (*rotateY - centerY);
  GLfloat tempY = sin(theta) * (*rotateX - centerX) + cos(theta) * (*rotateY - centerY);
  *rotateX = tempX + centerX;
  *rotateY = tempY + centerY;
}
(3)OpenGL 渲染基础与函数深度解析

理解项目中出现的每一个 OpenGL 函数及其参数,是掌握高性能图形渲染的核心:

  • glViewport(x, y, width, height):
    • 作用: 设置视口(Viewport),即 OpenGL 最终将渲染内容映射到屏幕上的矩形区域。
    • 参数: (x, y) 是视口左下角的起始位置,(width, height) 是视口的像素大小。它完成了从规范化设备坐标(-1 到 1)到屏幕像素坐标的转换。
  • glClearColor(r, g, b, a) & glClear(mask):
    • 作用: 前者设置用于清除颜色的“底漆”;后者执行实际的清除动作。
    • 参数: glClearmask 通常为 GL_COLOR_BUFFER_BIT,表示清除颜色缓冲区。
  • glUseProgram(program):
    • 作用: 激活指定的着色器程序对象。
    • 参数: program 是通过 glCreateProgram 链接生成的 ID。设置后,后续的顶点关联和绘制指令都在该程序下进行。
  • glGetAttribLocation(program, name):
    • 作用: 获取顶点着色器中 attribute 变量(如 a_position)的槽位索引(Location)。
  • glVertexAttribPointer(index, size, type, normalized, stride, pointer):
    • 作用: 描述顶点数据的内存布局,将 C++ 数组与着色器变量关联。
    • 参数:
      • index: 变量索引。
      • size: 每个顶点的分量数(如 (x, y) 取值为 2)。
      • type: 数据类型(如 GL_FLOAT)。
      • normalized: 是否对非浮点数据归一化。
      • stride: 步长,相邻顶点间的间隔字节数。
      • pointer: 指向内存中顶点数据的指针。
  • glEnableVertexAttribArray(index):
    • 作用: 启用指定索引的顶点属性。默认情况下所有属性是禁用的,必须手动开启才能在绘制时生效。
  • glVertexAttrib4fv(index, v):
    • 作用: 为指定的属性变量设置一个统一的(Uniform-like)常量值。在本项目中用于设置当前四边形的填充颜色。
  • glDrawArrays(mode, first, count):
    • 作用: 渲染图元的终极指令。
    • 参数:
      • mode: 渲染模式。
      • first: 起始索引。
      • count: 顶点数量。
    • 绘制模式 (mode) 详解:
      • GL_POINTS: 绘制独立的孤立点。
      • GL_LINES: 按对连接顶点,绘制独立线段。
      • GL_LINE_STRIP: 连接所有顶点,绘制一条连续线条。
      • GL_LINE_LOOP: 连成线段并闭合首尾。
      • GL_TRIANGLES: 每三个顶点构成一个独立三角形。
      • GL_TRIANGLE_FAN(本项目使用): 以第一个顶点为公共中心,连接后续所有顶点形成星扇形区域,非常适合绘制五角星这类凸多边形或复杂多边形的子集。
(4)绘制流程详细拆解:从 ExecuteDraw 到 GPU

核心绘制数据流向如下:

  1. 映射数据: 通过 glVertexAttribPointer 将 C++ 内存中的 shapeVertices 映射给着色器的 position 槽位。
  2. 触发绘制: 调用 glDrawArrays,GPU 根据当前绑定的程序、顶点数据和绘制模式(扇形填充)进行光栅化渲染。
  3. 循环迭代: 主循环执行 5 次,每次旋转角度并调用 ExecuteDraw
// 示例逻辑:五个部分依次绘制,每个部分使用调色盘中对应的颜色
GLfloat rad = M_PI / 180 * 72; // 72度
for (int i = 0; i < 5; ++i) {
    Rotate2d(centerX, centerY, &rotateX, &rotateY, rad * i); // 旋转数学计算
    // ... 映射顶点、设置颜色并触发绘制 ...
    ExecuteDraw(position, DRAW_PALETTE[i], shapeVertices, sizeof(shapeVertices));
}

完整绘制代码:

void EGLCore::Draw(int &hasDraw) {  
  flag_ = false;  
  OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "EGLCore", "Draw");  
  GLint position = PrepareDraw();  
  if (position == POSITION_ERROR) {  
    OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "EGLCore",  
                 "Draw get position failed");  
    return;  
  }  
  
  // 绘制背景  
  if (!ExecuteDraw(position, BACKGROUND_COLOR, BACKGROUND_RECTANGLE_VERTICES,  
                   sizeof(BACKGROUND_RECTANGLE_VERTICES))) {  
    OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "EGLCore",  
                 "Draw execute draw background failed");  
    return;  
  }  
  
  // 将五角星分为五个四边形,计算其中一个四边形的四个顶点  
  GLfloat rotateX = 0;  
  GLfloat rotateY = FIFTY_PERCENT * height_;  
  GLfloat centerX = 0;  
  // Convert DEG(54° & 18°) to RAD  
  GLfloat centerY = -rotateY * (M_PI / 180 * 54) * (M_PI / 180 * 18);  
  // Convert DEG(18°) to RAD  
  GLfloat leftX = -rotateY * (M_PI / 180 * 18);  
  GLfloat leftY = 0;  
  // Convert DEG(18°) to RAD  
  GLfloat rightX = rotateY * (M_PI / 180 * 18);  
  GLfloat rightY = 0;  
  
  // 确定绘制四边形的顶点,使用绘制区域的百分比表示  
  const GLfloat shapeVertices[] = {  
      centerX / width_, centerY / height_, leftX / width_,  leftY / height_,  
      rotateX / width_, rotateY / height_, rightX / width_, rightY / height_};  
  
  // 绘制图形 (第一个部分)  
  if (!ExecuteDraw(position, DRAW_PALETTE[0], shapeVertices,  
                   sizeof(shapeVertices))) {  
    OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "EGLCore",  
                 "Draw execute draw shape failed");  
    return;  
  }  
  
  // Convert DEG(72°) to RAD  
  GLfloat rad = M_PI / 180 * 72;  
  // Rotate four times  
  for (int i = 0; i < NUM_4; ++i) {  
    // 旋转得其他四个四边形的顶点  
    Rotate2d(centerX, centerY, &rotateX, &rotateY, rad);  
    Rotate2d(centerX, centerY, &leftX, &leftY, rad);  
    Rotate2d(centerX, centerY, &rightX, &rightY, rad);  
  
    // 确定绘制四边形的顶点,使用绘制区域的百分比表示  
    const GLfloat shapeVertices[] = {  
        centerX / width_, centerY / height_, leftX / width_,  leftY / height_,  
        rotateX / width_, rotateY / height_, rightX / width_, rightY / height_};  
  
    // 绘制图形 (后续四个部分)  
    if (!ExecuteDraw(position, DRAW_PALETTE[i + 1], shapeVertices,  
                     sizeof(shapeVertices))) {  
      OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "EGLCore",  
                   "Draw execute draw shape failed");  
      return;  
    }  
  }  
  
  // 结束绘制  
  if (!FinishDraw()) {  
    OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "EGLCore",  
                 "Draw FinishDraw failed");  
    return;  
  }  
  hasDraw = 1;  
  
  flag_ = true;  
}

绘制效果:
在这里插入图片描述

在这里插入图片描述

当点击切换颜色时修改颜色数组即可,修改颜色后效果:
在这里插入图片描述

多个XComponent验证

前面文章介绍过,当ArkTS层有多个XComponent时,C++中的Init函数会被调用多次,我们在Init函数中增加日志验证:

static napi_value Init(napi_env env, napi_value exports) {  
  PluginManager::GetInstance()->Export(env, exports);  
  return exports;  
}

void PluginManager::Export(napi_env env, napi_value exports) {  
    OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "PluginManager",  
                 "Export start");
	//...                 
}

日志确实被打印了两次:
![[HarmonyOS 6 自定义人脸识别模型3:OH_NativeXComponent基于OpenGL绘制-3.png]]

总结

通过 OH_NativeXComponent 结合 OpenGL,我们可以完全掌管 UI 组件的像素级渲染。核心步骤在于:

  1. 环境配置:利用 EGL 准备好渲染所需的 Surface 和 Context。
  2. 数据交互:在 C++ 层根据具体业务逻辑(如本文中的星形图案绘制)计算顶点和颜色。
  3. 渲染呈现:通过 OpenGL ES 指令绘制并利用 eglSwapBuffers 同步到 native 窗口。

掌握了这一套 OpenGL 渲染流程,我们就具备了在 HarmonyOS 上开发高性能游戏、自定义视觉特效乃至复杂的人脸特征点可视化的基础能力。示例代码地址:https://github.com/qingkouwei/NativeXComponent

Logo

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

更多推荐