前面文章《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.cppboolEGLCore::EglContextInit(void*window,intwidth,intheight){UpdateSize(width,height);eglWindow_=reinterpret_cast<EGLNativeWindowType>(window);// 1. 初始化 displayeglDisplay_=eglGetDisplay(EGL_DEFAULT_DISPLAY);// 2. 初始化 EGLEGLint majorVersion;EGLint minorVersion;if(!eglInitialize(eglDisplay_,&majorVersion,&minorVersion)){returnfalse;}// 3. 选择配置constEGLint maxConfigSize=1;EGLint numConfigs;if(!eglChooseConfig(eglDisplay_,ATTRIB_LIST,&eglConfig_,maxConfigSize,&numConfigs)){returnfalse;}// 4. 创建环境(Surface 和 Context)returnCreateEnvironment();}boolEGLCore::CreateEnvironment(){// 创建 SurfaceeglSurface_=eglCreateWindowSurface(eglDisplay_,eglConfig_,eglWindow_,NULL);// 创建 ContexteglContext_=eglCreateContext(eglDisplay_,eglConfig_,EGL_NO_CONTEXT,CONTEXT_ATTRIBS);// 绑定当前线程if(!eglMakeCurrent(eglDisplay_,eglSurface_,eglSurface_,eglContext_)){returnfalse;}// 创建着色器程序 (Program)program_=CreateProgram(VERTEX_SHADER,FRAGMENT_SHADER);returntrue;}2. 执行渲染逻辑
渲染时,我们需要通过glUseProgram激活程序,并向顶点着色器传递顶点坐标和颜色数据。
// egl_core.cppvoidEGLCore::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;}boolEGLCore::FinishDraw(){glFlush();glFinish();// 将绘制内容“展示”到屏幕上returneglSwapBuffers(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.cppvoidOnSurfaceCreatedCB(OH_NativeXComponent*component,void*window){// ... 获取 id 和 size ...if(render->eglCore_->EglContextInit(window,width,height)){render->eglCore_->Background();// 初始背景色渲染}}// 供 ArkTS 层调用的绘制方法napi_valuePluginRender::NapiDrawPattern(napi_env env,napi_callback_info info){// ... 获取 PluginRender 实例 ...render->eglCore_->Draw(hasDraw_);returnnullptr;}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函数中:
voidEGLCore::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(inti=0;i<5;++i){Rotate2d(centerX,centerY,&rotateX,&rotateY,rad*i);// 旋转数学计算// ... 映射顶点、设置颜色并触发绘制 ...ExecuteDraw(position,DRAW_PALETTE[i],shapeVertices,sizeof(shapeVertices));}完整绘制代码:
voidEGLCore::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 RADGLfloat centerY=-rotateY*(M_PI/180*54)*(M_PI/180*18);// Convert DEG(18°) to RADGLfloat leftX=-rotateY*(M_PI/180*18);GLfloat leftY=0;// Convert DEG(18°) to RADGLfloat rightX=rotateY*(M_PI/180*18);GLfloat rightY=0;// 确定绘制四边形的顶点,使用绘制区域的百分比表示constGLfloat 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 RADGLfloat rad=M_PI/180*72;// Rotate four timesfor(inti=0;i<NUM_4;++i){// 旋转得其他四个四边形的顶点Rotate2d(centerX,centerY,&rotateX,&rotateY,rad);Rotate2d(centerX,centerY,&leftX,&leftY,rad);Rotate2d(centerX,centerY,&rightX,&rightY,rad);// 确定绘制四边形的顶点,使用绘制区域的百分比表示constGLfloat 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