一、创建工程
File>New>Create Project,创建个新native c++工程出来。
页面布局在entry\src\main\ets\pages\Index.ets中,这是Ark-TS写的页面布局,显示“Hello World”字符串,且点击字符时,会用Node-api调用C++计算加法,并将结果返回到Ark-JS进行log输出。
import testNapi from 'libentry.so'
@Entry
@Component
struct Index {
@State message: string = 'Hello World'
build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
.onClick(() => {
hilog.info(0x0000, 'testTag', 'Test NAPI 2 + 3 = %{public}d', testNapi.add(2, 3));
})
}
.width('100%')
}
.height('100%')
}
}
napi的写法具体可以参考这里:使用Node-API实现跨语言交互开发流程。
二、添加XComponent
写安卓图形应用,使用C api方式,一般是先从NDK获取windows(可能是设置回调,也可能是直接调用接口获取),用windows初始化EGL,获取Surface:
EGLNativeDisplayType ndt = NULL;
EGLNativeWindowType nwh; //Window
EGLDisplay display = eglGetDisplay(NULL == ndt ? EGL_DEFAULT_DISPLAY : ndt);
EGLSurface surface = eglCreateWindowSurface(display, config, nwh, NULL);
EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, s_contextAttrs);
eglMakeCurrent(display, surface, surface, context);
设置布局
安卓和鸿蒙的差异会主要体现在上述nwh的获取,对于鸿蒙应用来说,会用XComponent组件在上述Index.ets中设置布局,并设置库名称:
build() {
Stack({ alignContent: Alignment.TopStart }) {
XComponent({
id: "test001",
type: XComponentType.SURFACE,
libraryname: "testgles"
})
.onLoad(() => {
hilog.info(0x0000, 'testTag', 'XComponent onLoad');
})
.width('100%')
.height('100%')
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
.onClick(() => {
hilog.info(0x0000, 'testTag', 'Test NAPI 2 + 3 = %{public}d', testNapi.add(2, 3));
})
}
.width('100%')
}
.height('100%')
}
}
上面代码libraryname: "testgles"
就是我设置的库名称,C++代码需要编译出libtestgles.so供其加载,因此需要在CMakeLists.txt中,进行相应改写,add_library
需要将其内容改成新的:
#add_library(entry SHARED napi_init.cpp)
#target_link_libraries(entry PUBLIC libace_napi.z.so)
add_library(testgles SHARED napi_init.cpp)
target_link_libraries(testgles PUBLIC libace_napi.z.so)#提供native_api.h的实现
napi_init.cpp里注册模块的地方也需要调整:
static napi_module demoModule = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = Init,
//.nm_modname = "entry",
.nm_modname = "testgles",
.nm_priv = ((void*)0),
.reserved = { 0 },
};
Init里暂且不需要注册给ets调用的函数,但是得注册XComponent回调来获取窗口。
添加log
我们得先加点log用于查看是否调用,引入log.h文件来完成:
#include "hilog/log.h"
#define APP_LOG_TAG "TEST GLES Demo"
#define LOGI(...) ((void)OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, APP_LOG_TAG, __VA_ARGS__))
#define LOGE(...) ((void)OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, APP_LOG_TAG, __VA_ARGS__))
log文件不是自动链接的,还要去CMakeList.txt中添加链接:
find_library(
hilog-lib
hilog_ndk.z
)
#提供native_api.h的实现
find_library(
libnapi-lib
ace_napi.z
)
target_link_libraries(testgles PUBLIC ${hilog-lib} ${libnapi-lib})
有了LOG,我们先简单定义四个事件回调函数:
void OnSurfaceCreatedCB(OH_NativeXComponent *component, void *window)
{
LOGI("OnSurfaceCrateCB");
}
void OnSurfaceChangedCB(OH_NativeXComponent *component, void *window)
{
LOGI("OnSurfaceChangedCB");
}
void OnSurfaceDestroyedCB(OH_NativeXComponent *component, void *window)
{
LOGI("OnSurfaceDestroyedCB");
}
void DispatchTouchEventCB(OH_NativeXComponent *component, void *window) {}
然后在C++用napi获取原生XComponent组件对象:
OH_NativeXComponent* GetNativeXComponent(napi_env env, napi_value exports)
{
//获取exportInstance函数
napi_value exportInstance = nullptr;
if (napi_ok != napi_get_named_property(env, exports, OH_NATIVE_XCOMPONENT_OBJ, &exportInstance)) {
LOGE("export napi_get_named_property fail");
return nullptr;
}
//调用函数获取XComponent
OH_NativeXComponent *nativeXComponent = nullptr;
if (napi_ok != napi_unwrap(env, exportInstance, reinterpret_cast<void **>(&nativeXComponent))) {
LOGE("export napi_unwrap fail");
return nullptr;
}
return nativeXComponent;
}
在Init中设置回调:
#include <ace/xcomponent/native_interface_xcomponent.h>
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports)
{
OH_NativeXComponent* nativeXComponent = GetNativeXComponent(env, exports);
if (nativeXComponent != nullptr)
{
static OH_NativeXComponent_Callback renderCallback;
renderCallback.OnSurfaceCreated = OnSurfaceCreatedCB;
renderCallback.OnSurfaceChanged = OnSurfaceChangedCB;
renderCallback.OnSurfaceDestroyed = OnSurfaceDestroyedCB;
renderCallback.DispatchTouchEvent = DispatchTouchEventCB;
if (OH_NATIVEXCOMPONENT_RESULT_SUCCESS != OH_NativeXComponent_RegisterCallback(nativeXComponent, &renderCallback))
{
LOGE("OH_NativeXComponent_RegisterCallback napi_get_named_property fail");
}
}
return exports;
}
EXTERN_C_END
注意我这里的renderCallback变量是static的,因为如果不定义static,其生命周期在出函数后就会结束,结束后里面注册的回调也会找不到(想不到华为这个函数存的竟然是结构体的地址而不是转存回调地址)实际编写中,renderCallback未必是static的,但一定是持久持有在某处内存中,肯定不能是函数中声明的暂态对象。
OH_NativeXComponent_RegisterCallback
在头文件ace/xcomponent/native_interface_xcomponent.h
中声明,并且也要在CMakeList.txt中链接文件:
find_library(
libace-lib
ace_ndk.z
)
target_link_libraries(testgles PUBLIC ${hilog-lib} ${libnapi-lib} ${libace-lib})
此时可以先编译并部署到设备中查看,是一个黑屏的app,且在开启时会输出OnSurfaceCreated回调中的logOnSurfaceCrateCB
,关闭app会输出OnSurfaceDestroyed中的logOnSurfaceDestroyedCB
。
三、EGL初始化
EGL用于管理窗口和上下文,获取渲染的backbuffer,我们在OnSurfaceCreatedCB
回调中可以获取到窗口,在这里初始化EGL,在这里启动渲染线程并初始化EGL。
我定义了一个新类:
class Renderer {
public:
void RenderThread();
void Close();
OH_NativeXComponent *mComponent;
void *mWindow;
private:
bool mStop = false;
bool mEglInit = false;
};
在OnSurfaceCreatedCB
中设置窗口和组件,并启动渲染线程:
Renderer renderer;
std::thread renderThread;
void OnSurfaceCreatedCB(OH_NativeXComponent *component, void *window)
{
LOGI("OnSurfaceCrateCB");
renderer.mWindow = window;
renderer.mComponent = component;
renderThread = std::thread(std::bind(&Renderer::RenderThread, renderer));
}
渲染线程循环执行渲染,不过我们先提供EGL初始化,并每帧输出LOG:
void Renderer::RenderThread()
{
while (!mStop)
{
if (!mEglInit && mWindow != NULL)
{
EGLInit(mWindow);
mEglInit = true;
}
if (mEglInit)
{
DrawLoop();
}
}
}
void DrawLoop()
{
LOGI("Draw……");
eglSwapBuffers(eglDisplay, eglSurface);
}
EGL初始化的方法较安卓上EGL初始化没有区别:
#include <EGL/egl.h>
EGLDisplay eglDisplay;
EGLSurface eglSurface;
void EGLInit(void *window)
{
//EGLNativeWindowType eglWindow = static_cast<EGLNativeWindowType>(window);
EGLNativeWindowType eglWindow = (EGLNativeWindowType)window;
eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (EGL_NO_DISPLAY == eglDisplay) {
LOGE("EGLCore init failed: unable to get EGL display");
return;
}
EGLint majorVersion;
EGLint minorVersion;
if (!eglInitialize(eglDisplay, &majorVersion, &minorVersion)) {
LOGE("EGLCore init failed: unable to get initialize EGL display");
return;
}
const EGLint maxConfigSize = 1;
const EGLint ATTRIB_LIST[] = {
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_ALPHA_SIZE, 8,
EGL_DEPTH_SIZE, 24,
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
EGL_NONE
};
EGLint numConfigs;
EGLConfig eglConfig;
if (!eglChooseConfig(eglDisplay, ATTRIB_LIST, &eglConfig, maxConfigSize, &numConfigs)) {
LOGE("EGLCore init failed: eglChooseConfig unable to choose configs");
return;
}
eglSurface = eglCreateWindowSurface(eglDisplay, eglConfig, eglWindow, NULL);
if (nullptr == eglSurface) {
LOGE("EGLCore init failed: eglCreateWindowSurface unable to create surface");
return;
}
const EGLint CONTEXT_ATTRIBS[] = {
EGL_CONTEXT_CLIENT_VERSION, 2,
EGL_NONE
};
EGLContext eglContext = eglCreateContext(eglDisplay, eglConfig, EGL_NO_CONTEXT, CONTEXT_ATTRIBS);
if (!eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) {
LOGE("EGLCore init failed: eglMakeCurrent failed");
return;
}
}
CMakeList.txt需要添加egl的链接:
find_library(
EGL-lib
EGL
)
target_link_libraries(testgles PUBLIC ${hilog-lib} ${libnapi-lib} ${libace-lib} ${EGL-lib})
顺带增加下停止逻辑:
void OnSurfaceDestroyedCB(OH_NativeXComponent *component, void *window)
{
LOGI("OnSurfaceDestroyedCB");
renderer.Close();
}
void Renderer::Close()
{
mStop = true;
}
现阶段就是一个循环输出LOG黑屏的应用。
四、GLES渲染编写
GLES的编写也没有任何区别,我直接从一个安卓实例拷贝出一个简单的GLES工程,不需要仔细看其中的逻辑,只要保证初始化、更新、销毁逻辑对应上即可:
#include <GLES3/gl32.h>
shader代码中写死:
static const char VERTEX_SHADER[] =
"#version 300 es\n"
"layout(location = " STRV(POS_ATTRIB) ") in vec2 pos;\n"
"layout(location=" STRV(COLOR_ATTRIB) ") in vec4 color;\n"
"layout(location=" STRV(SCALEROT_ATTRIB) ") in vec4 scaleRot;\n"
"layout(location=" STRV(OFFSET_ATTRIB) ") in vec2 offset;\n"
"out vec4 vColor;\n"
"void main() {\n"
" mat2 sr = mat2(scaleRot.xy, scaleRot.zw);\n"
" gl_Position = vec4(sr*pos + offset, 0.0, 1.0);\n"
" vColor = color;\n"
"}\n";
static const char FRAGMENT_SHADER[] =
"#version 300 es\n"
"precision mediump float;\n"
"in vec4 vColor;\n"
"out vec4 outColor;\n"
"void main() {\n"
" outColor = vColor;\n"
"}\n";
一些简单的代码设施:
bool checkGlError(const char* funcName) {
GLint err = glGetError();
if (err != GL_NO_ERROR) {
LOGE("GL error after %s(): 0x%08x\n", funcName, err);
return true;
}
return false;
}
GLuint createShader(GLenum shaderType, const char* src) {
GLuint shader = glCreateShader(shaderType);
if (!shader) {
checkGlError("glCreateShader");
return 0;
}
glShaderSource(shader, 1, &src, NULL);
GLint compiled = GL_FALSE;
glCompileShader(shader);
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (!compiled) {
GLint infoLogLen = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLogLen);
if (infoLogLen > 0) {
GLchar* infoLog = (GLchar*)malloc(infoLogLen);
if (infoLog) {
glGetShaderInfoLog(shader, infoLogLen, NULL, infoLog);
LOGE("Could not compile %s shader:\n%s\n",
shaderType == GL_VERTEX_SHADER ? "vertex" : "fragment", infoLog);
free(infoLog);
}
}
glDeleteShader(shader);
return 0;
}
return shader;
}
GLuint createProgram(const char* vtxSrc, const char* fragSrc) {
GLuint vtxShader = 0;
GLuint fragShader = 0;
GLuint program = 0;
GLint linked = GL_FALSE;
vtxShader = createShader(GL_VERTEX_SHADER, vtxSrc);
if (!vtxShader) goto exit;
fragShader = createShader(GL_FRAGMENT_SHADER, fragSrc);
if (!fragShader) goto exit;
program = glCreateProgram();
if (!program) {
checkGlError("glCreateProgram");
goto exit;
}
glAttachShader(program, vtxShader);
glAttachShader(program, fragShader);
glLinkProgram(program);
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (!linked) {
LOGE("Could not link program");
GLint infoLogLen = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLogLen);
if (infoLogLen) {
GLchar* infoLog = (GLchar*)malloc(infoLogLen);
if (infoLog) {
glGetProgramInfoLog(program, infoLogLen, NULL, infoLog);
LOGE("Could not link program:\n%s\n", infoLog);
free(infoLog);
}
}
glDeleteProgram(program);
program = 0;
}
exit:
glDeleteShader(vtxShader);
glDeleteShader(fragShader);
return program;
}
初始化资产:
GLuint shaderProgram;
enum { VB_INSTANCE, VB_SCALEROT, VB_OFFSET, VB_COUNT };
#define MAX_INSTANCES_PER_SIDE 16
#define MAX_INSTANCES (MAX_INSTANCES_PER_SIDE * MAX_INSTANCES_PER_SIDE)
#define TWO_PI (2.0 * M_PI)
#define MAX_ROT_SPEED (0.3 * TWO_PI)
struct Vertex {
GLfloat pos[2];
GLubyte rgba[4];
};
Vertex QUAD[4] = {
// Square with diagonal < 2 so that it fits in a [-1 .. 1]^2 square
// regardless of rotation.
{{-0.7f, -0.7f}, {0x00, 0xFF, 0x00}},
{{0.7f, -0.7f}, {0x00, 0x00, 0xFF}},
{{-0.7f, 0.7f}, {0xFF, 0x00, 0x00}},
{{0.7f, 0.7f}, {0xFF, 0xFF, 0xFF}},
};
GLuint mVB[VB_COUNT];
GLuint mVBState;
float* mapOffsetBuf() {
glBindBuffer(GL_ARRAY_BUFFER, mVB[VB_OFFSET]);
return (float*)glMapBufferRange(
GL_ARRAY_BUFFER, 0, MAX_INSTANCES * 2 * sizeof(float),
GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT);
}
void unmapOffsetBuf() { glUnmapBuffer(GL_ARRAY_BUFFER); }
unsigned int mNumInstances;
float mScale[2];
float mAngularVelocity[MAX_INSTANCES];
uint64_t mLastFrameNs;
float mAngles[MAX_INSTANCES];
void calcSceneParams(unsigned int w, unsigned int h, float* offsets) {
// number of cells along the larger screen dimension
const float NCELLS_MAJOR = MAX_INSTANCES_PER_SIDE;
// cell size in scene space
const float CELL_SIZE = 2.0f / NCELLS_MAJOR;
// Calculations are done in "landscape", i.e. assuming dim[0] >= dim[1].
// Only at the end are values put in the opposite order if h > w.
const float dim[2] = {fmaxf(w, h), fminf(w, h)};
const float aspect[2] = {dim[0] / dim[1], dim[1] / dim[0]};
const float scene2clip[2] = {1.0f, aspect[0]};
const int ncells[2] = {static_cast<int>(NCELLS_MAJOR),
(int)floorf(NCELLS_MAJOR * aspect[1])};
float centers[2][MAX_INSTANCES_PER_SIDE];
for (int d = 0; d < 2; d++) {
auto offset = -ncells[d] / NCELLS_MAJOR; // -1.0 for d=0
for (auto i = 0; i < ncells[d]; i++) {
centers[d][i] = scene2clip[d] * (CELL_SIZE * (i + 0.5f) + offset);
}
}
int major = w >= h ? 0 : 1;
int minor = w >= h ? 1 : 0;
// outer product of centers[0] and centers[1]
for (int i = 0; i < ncells[0]; i++) {
for (int j = 0; j < ncells[1]; j++) {
int idx = i * ncells[1] + j;
offsets[2 * idx + major] = centers[0][i];
offsets[2 * idx + minor] = centers[1][j];
}
}
mNumInstances = ncells[0] * ncells[1];
mScale[major] = 0.5f * CELL_SIZE * scene2clip[0];
mScale[minor] = 0.5f * CELL_SIZE * scene2clip[1];
}
void GLESResize(int w, int h)
{
auto offsets = mapOffsetBuf();
calcSceneParams(w, h, offsets);
unmapOffsetBuf();
// Auto gives a signed int :-(
for (auto i = (unsigned)0; i < mNumInstances; i++) {
mAngles[i] = drand48() * TWO_PI;
mAngularVelocity[i] = MAX_ROT_SPEED * (2.0 * drand48() - 1.0);
}
mLastFrameNs = 0;
glViewport(0, 0, w, h);
}
void GLESInit(int w, int h)
{
shaderProgram = createProgram(VERTEX_SHADER, FRAGMENT_SHADER);
glGenBuffers(VB_COUNT, mVB);
glBindBuffer(GL_ARRAY_BUFFER, mVB[VB_INSTANCE]);
glBufferData(GL_ARRAY_BUFFER, sizeof(QUAD), &QUAD[0], GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, mVB[VB_SCALEROT]);
glBufferData(GL_ARRAY_BUFFER, MAX_INSTANCES * 4 * sizeof(float), NULL,
GL_DYNAMIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, mVB[VB_OFFSET]);
glBufferData(GL_ARRAY_BUFFER, MAX_INSTANCES * 2 * sizeof(float), NULL,
GL_STATIC_DRAW);
glGenVertexArrays(1, &mVBState);
glBindVertexArray(mVBState);
glBindBuffer(GL_ARRAY_BUFFER, mVB[VB_INSTANCE]);
glVertexAttribPointer(POS_ATTRIB, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
(const GLvoid*)offsetof(Vertex, pos));
glVertexAttribPointer(COLOR_ATTRIB, 4, GL_UNSIGNED_BYTE, GL_TRUE,
sizeof(Vertex), (const GLvoid*)offsetof(Vertex, rgba));
glEnableVertexAttribArray(POS_ATTRIB);
glEnableVertexAttribArray(COLOR_ATTRIB);
glBindBuffer(GL_ARRAY_BUFFER, mVB[VB_SCALEROT]);
glVertexAttribPointer(SCALEROT_ATTRIB, 4, GL_FLOAT, GL_FALSE,
4 * sizeof(float), 0);
glEnableVertexAttribArray(SCALEROT_ATTRIB);
glVertexAttribDivisor(SCALEROT_ATTRIB, 1);
glBindBuffer(GL_ARRAY_BUFFER, mVB[VB_OFFSET]);
glVertexAttribPointer(OFFSET_ATTRIB, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float),
0);
glEnableVertexAttribArray(OFFSET_ATTRIB);
glVertexAttribDivisor(OFFSET_ATTRIB, 1);
GLESResize(w, h);
}
销毁资产
void GLESRelease()
{
glDeleteVertexArrays(1, &mVBState);
glDeleteBuffers(VB_COUNT, mVB);
glDeleteProgram(shaderProgram);
}
更新:
float* mapTransformBuf() {
glBindBuffer(GL_ARRAY_BUFFER, mVB[VB_SCALEROT]);
return (float*)glMapBufferRange(
GL_ARRAY_BUFFER, 0, MAX_INSTANCES * 4 * sizeof(float),
GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT);
}
void unmapTransformBuf() { glUnmapBuffer(GL_ARRAY_BUFFER); }
void step() {
timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
auto nowNs = now.tv_sec * 1000000000ull + now.tv_nsec;
if (mLastFrameNs > 0) {
float dt = float(nowNs - mLastFrameNs) * 0.000000001f;
for (unsigned int i = 0; i < mNumInstances; i++) {
mAngles[i] += mAngularVelocity[i] * dt;
if (mAngles[i] >= TWO_PI) {
mAngles[i] -= TWO_PI;
} else if (mAngles[i] <= -TWO_PI) {
mAngles[i] += TWO_PI;
}
}
float* transforms = mapTransformBuf();
for (unsigned int i = 0; i < mNumInstances; i++) {
float s = sinf(mAngles[i]);
float c = cosf(mAngles[i]);
transforms[4 * i + 0] = c * mScale[0];
transforms[4 * i + 1] = s * mScale[1];
transforms[4 * i + 2] = -s * mScale[0];
transforms[4 * i + 3] = c * mScale[1];
}
unmapTransformBuf();
}
mLastFrameNs = nowNs;
}
将上述逻辑与现有代码结合,首先是初始化和销毁:
void Renderer::RenderThread()
{
while (!mStop)
{
if (!mEglInit && mWindow != NULL)
{
EGLInit(mWindow);
uint64_t width;
uint64_t height;
int32_t ret = OH_NativeXComponent_GetXComponentSize(mComponent, mWindow, &width, &height);
GLESInit(width, height);
mEglInit = true;
}
if (mEglInit)
{
DrawLoop();
}
}
GLESRelease();
}
然后是更新:
void DrawLoop()
{
step();
glClearColor(0.2f, 0.2f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
LOGI("Draw……");
glUseProgram(shaderProgram);
glBindVertexArray(mVBState);
glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, mNumInstances);
eglSwapBuffers(eglDisplay, eglSurface);
}
CMakeList.txt别忘链接gles:
find_library(
GLES-lib
GLESv3
)
target_link_libraries(testgles PUBLIC ${hilog-lib} ${libnapi-lib} ${libace-lib} ${EGL-lib} ${GLES-lib})
这样就简单将一个安卓下的图形示例移植到鸿蒙下:
总结
在渲染功能这一块,安卓和鸿蒙的写法差异主要体现在window的获取上,egl和gles上没有什么区别。