HarmonyOS 6 自定义人脸识别模型3:OH_NativeXComponent基于OpenGL绘制
摘要:本文详细介绍了在HarmonyOS 6中通过OpenGL ES在OH_NativeXComponent上实现图形绘制的完整流程。主要内容包括:1) OpenGL ES和EGL的基本概念及其在移动开发中的作用;2) HarmonyOS NDK中使用OpenGL的标准操作流程;3) 具体实现方案,通过EGLCore类封装EGL环境初始化、渲染逻辑和缓冲区交换,并在PluginRender类中与O
前面文章《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 进行绘制通常遵循以下标准流程:
- 获取原生窗口句柄:通过
OH_NativeXComponent获取底层的NativeWindow。 - 创建 EGL Display:建立与本地窗口系统的连接。
- 初始化 EGL:设置 EGL 的版本信息。
- 选择 EGL Config:配置渲染参数(如颜色位宽、采样率等)。
- 创建 EGL Surface:将
NativeWindow绑定到 EGL。 - 创建 EGL Context:创建渲染状态机。
- 绑定上下文(Make Current):将当前线程与 EGL 上下文绑定。
- 执行 OpenGL 渲染指令:使用渲染程序(Shader Program)进行绘图。
- 交换缓冲区(Swap Buffers):将渲染内容显示到屏幕上。
OpenGL基于OH_NativeXComponent绘制
接下来详细介绍如何按照上面步骤实现具体的绘制流程,这里我们把主要逻辑封装在 EGLCore 和 PluginRender 类中。
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):- 作用: 前者设置用于清除颜色的“底漆”;后者执行实际的清除动作。
- 参数:
glClear的mask通常为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
核心绘制数据流向如下:
- 映射数据: 通过
glVertexAttribPointer将 C++ 内存中的shapeVertices映射给着色器的position槽位。 - 触发绘制: 调用
glDrawArrays,GPU 根据当前绑定的程序、顶点数据和绘制模式(扇形填充)进行光栅化渲染。 - 循环迭代: 主循环执行 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 组件的像素级渲染。核心步骤在于:
- 环境配置:利用 EGL 准备好渲染所需的 Surface 和 Context。
- 数据交互:在 C++ 层根据具体业务逻辑(如本文中的星形图案绘制)计算顶点和颜色。
- 渲染呈现:通过 OpenGL ES 指令绘制并利用
eglSwapBuffers同步到 native 窗口。
掌握了这一套 OpenGL 渲染流程,我们就具备了在 HarmonyOS 上开发高性能游戏、自定义视觉特效乃至复杂的人脸特征点可视化的基础能力。示例代码地址:https://github.com/qingkouwei/NativeXComponent
更多推荐



所有评论(0)