前言
- lib3mf 集成系列(一):编译与示例测试
- lib3mf 集成系列(二):Flutter FFI 插件创建与 Android 端集成
- lib3mf 集成系列(三):在 Flutter 中解析 3MF 文件信息
- lib3mf 集成系列(四):Flutter Texture 与 C++ OpenGL 跨端渲染架构
- lib3mf 集成系列(五):C++ 提取 3MF 数据与 OpenGL 渲染实战
在本系列的上一篇文章中,我们成功地在 C++ 底层解析到了 3MF 的模型数据,但尚未将其渲染并展示到页面上。 而在本篇文章,将介绍如何使用解析到的数据,通过 OpenGL 渲染出3D模型。
本文可能需要你阅读过之前的一篇文章Flutter 嵌入安卓原生 View,以及与原生交互。在那篇文章里,我们曾通过 PlatformView 顺利嵌入了普通的 Android 控件,因此原本也自然地打算故技重施,直接嵌入一个 GLSurfaceView 来做 3D 渲染。但我们在真实的尝试中直接撞了“南墙”:
GLSurfaceView 的本质是在 Android 原生的 View 树上强行“挖一个透底的洞”,它的 Surface 享有独立的 Z-Order 层级。这与 Flutter 基于引擎(Skia/Impeller)单层自绘渲染的管线发生了严重的哲学冲突。如果你强行在 PlatformView (甚至使用 Hybrid-Composition) 里塞入一个极高频自重绘的 EGL 表面,面临的将是:层叠错乱、黑屏闪烁、两端(Android/Flutter)帧同步完全撕裂,以至于滑动时出现严重的割裂感。
因此,为了获得真正无痛的原生性能,我们选择“推倒重来”,全盘切向 Flutter 官方在视频 / 相机流处理中最推荐的高频渲染王道方案——外部纹理(Texture组件)。结合原生端的 SurfaceTexture 以及全手动管理的 EGL 上下文 与后台帧渲染线程,我们能将底层计算好的 3D 画面直接投递给 Flutter 的自绘管线中,既无需“挖洞”,也没有任何多余的层级开销。本文涉及的核心实现正是基于下文 model_file_picker_route.dart (见文章末尾的Gist 链接) 所重构的方案。
OpenGL 下放思路(Flutter -> Android -> JNI -> C++)
我们的核心思路是将 Flutter 中的外部纹理(Texture Widget)传递给安卓原生层。而在原生层我们不会直接新建一个带生命周期的 View,而是依托于 TextureRegistry 申请一块纹理,并借助 HandlerThread 及 EGL 自行管理上下文。随后,原生的 EGL 线程会将 OpenGL 的生命周期事件(创建、尺寸变更、逐帧渲染)通过 JNI 转发给底层的 C++。完整步骤如下:
- Flutter 申请纹理并获取 ID: 通过
MethodChannel唤起 Android 原生(Kotlin)环境执行纹理创建。 - Android 原生创建 SurfaceTexture 与 EGL 上下文: 注册获得
SurfaceTexture,基于它创建EGLSurface并在后台渲染线程中makeCurrent。 - 基于 Choreographer 同步刷新: 取代
GLSurfaceView自带的渲染管理,使用系统的 Choreographer 获取 VSYNC 信号,驱动底层onDrawFrame。 - JNI 抛出声明周期给 C++: Android 则通过
NativeDrawerRenderer调用 C++ 层暴露好的NativeRenderBridge,由最底层的SceneRenderer完成绘制。
Flutter 端实现:Texture Widget 与 MethodChannel 桥接
在 model_file_picker_route.dart 中,我们需要获取一个原生的 Texture ID 并通过该 ID 将其嵌入到组件树中:
// 请求 Android 原生层分配 Texture
Future<void> _setupNativeTexture() async {
final id = await MethodChannel(
"com.chaosgoo.metasequoia/gl_texture2",
).invokeMethod('createGLTexture', {'width': 512, 'height': 512});
setState(() {
_textureId = id;
});
}
紧接着,在界面构建部分,只要 _textureId 就绪,我们就能用 Texture Widget 直接挂载:
// 借助 LayoutBuilder 侦测外部约束变化
LayoutBuilder(
builder: (context, constraints) {
final double pixelRatio = MediaQuery.of(context).devicePixelRatio;
final double width = constraints.maxWidth * pixelRatio;
final double height = constraints.maxHeight * pixelRatio;
_updateNativeTexture(width.toInt(), height.toInt());
return GestureDetector(
onPanUpdate: (details) {
const sensitivity = 0.01;
MethodChannel("com.chaosgoo.metasequoia/gl_texture2")
.invokeMethod('rotate', {
'textureId': _textureId,
'angleX': details.delta.dx * sensitivity,
'angleY': details.delta.dy * sensitivity,
});
},
child: Texture(textureId: _textureId!),
);
},
)
通过 GestureDetector 以及手势捕捉函数 onPanUpdate,也能将触摸偏移量以坐标轴旋转形式传递到 MethodChannel。
Android 端:纹理管理与 EGL 后台线程
进入 Android 层,我们需要接受 Flutter 请求并开辟自己的 OpenGL 渲染环境。
首先,我们在 GLTexturePlugin2 插件中接受请求和路由处理:
"createGLTexture" -> {
val handler = GLTextureHandler2(textureRegistry, emptyMap())
val width = call.argument<Int>("width") ?: 640
val height = call.argument<Int>("height") ?: 640
val textureId = handler.getTextureId()
textureHandlers[textureId] = handler
handler.setup(width, height)
result.success(textureId)
}
由于我们脱离了 GLSurfaceView,因此在 GLTextureHandler2 中,我们必须搭建出一个独立的 HandlerThread 以及 EGL 环境。一旦建立完毕,使用系统的 VSync 获取帧刷新时机(通过 Choreographer.getInstance())。
// Choreographer.FrameCallback
private val frameCallback = object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
if (!isRendering) return
renderThreadHandler.sendEmptyMessage(DRAW)
choreographer.postFrameCallback(this) // 继续监听下一帧
}
}
// 核心 Handler 处理渲染生命周期逻辑
val cb = Handler.Callback { msg ->
when (msg.what) {
INIT -> {
Surface(surfaceTexture).apply {
surface = this
eglCore = EglCore(this).apply { makeCurrent() }
render = NativeDrawerRenderer()
render?.onSurfaceCreated(null, null)
val (width, height) = msg.obj as? Pair<Int, Int> ?: (0 to 0)
render?.onSurfaceChanged(null, width, height)
}
start() // 开始监听 Choreographer VSYNC
}
DRAW -> {
render?.onDrawFrame(null)
eglCore?.swapBuffer() // 更新到屏幕/Texture中
}
LOAD_MODEL -> {
val modelPath = msg.obj as? String ?: ""
(render as? NativeDrawerRenderer)?.loadModel(modelPath)
}
// ... 其他命令 (UPDATE_SIZE, ROTATE 等)
}
return@Callback true
}
这样的设计将大量的计算和渲染逻辑扔到了后台线程(RenderThread),且因为我们在最初绑定了 Flutter 的 SurfaceTexture,当 eglCore?.swapBuffer() 触发时,Flutter 会自动监听到数据变更并通过内部流完成渲染上屏。
JNI 编写:与底层 OpenGL 联通
在 Android Kotlin 层面 (NativeDrawerRenderer.kt),它仅仅是一个下辖至 C++ 的简单封装 Wrapper:
class NativeDrawerRenderer() : GLSurfaceView.Renderer {
override fun onDrawFrame(gl: GL10?) {
NativeRenderBridge.onDrawFrame(gl)
}
//... 其他事件的封装
}
由于要连通底层的 C++ 并调用 3MF 解析和各种 OpenGL 指令,我们会再通过 NativeRenderBridge.java 直接进入 C++ 层的实现。例如 JNI 侧 (native_renderer_bridge.cpp) 便接管了模型生命周期:
static SceneRenderer *g_renderer = nullptr;
extern "C" JNIEXPORT void JNICALL
Java_com_example_native_1lib3mf_NativeRenderBridge_onSurfaceCreated(
JNIEnv *env, jclass clazz, jobject gl, jobject config) {
if (g_renderer) {
delete g_renderer;
}
g_renderer = new SceneRenderer();
g_renderer->init(getAssetManager());
}
extern "C" JNIEXPORT void JNICALL
Java_com_example_native_1lib3mf_NativeRenderBridge_onDrawFrame(JNIEnv *env,
jclass thiz,
jobject gl) {
if (g_renderer) {
g_renderer->render();
}
}
// 对应我们在模型选择路由中的模型加载事件
extern "C" JNIEXPORT void JNICALL
Java_com_example_native_1lib3mf_NativeRenderBridge_loadModel(JNIEnv *env, jclass clazz,
jstring path) {
const char *nativePath = env->GetStringUTFChars(path, nullptr);
if (g_renderer) {
g_renderer->loadModel(nativePath);
}
env->ReleaseStringUTFChars(path, nativePath);
}
阶段性首飞:清屏与基础三角形测试
为了验证我们搭建的 Flutter -> Android -> C++ 链路是否已经真正活着打通,我们可以在 C++ 底层的 onDrawFrame 钩子内,先不急着处理庞大的 3MF 数据,而是编写一段最古朴的 OpenGL ES “Hello Triangle” 或清屏测试代码:
extern "C" JNIEXPORT void JNICALL
Java_com_example_native_1lib3mf_NativeRenderBridge_onDrawFrame(JNIEnv *env,
jclass thiz,
jobject gl) {
// 1. 最基础的清屏测试:将背景清空为红色,验证 EGL 上下文与 VSync 是通畅的
glClearColor(1.0f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 2. 基础三角形测试(可选步骤)
// 你可以在此地硬编码三个顶点坐标(如 0.0, 0.5, 0.0),并加载极其简单的无矩阵 Shader
// 使用 glDrawArrays(GL_TRIANGLES, 0, 3) 画出一个占据屏幕中心的基础三角形。
// 如果 g_renderer 已就绪,也可以将上述写死测试的代码封装在里面
if (g_renderer) {
// g_renderer->renderTestTriangle();
g_renderer->render();
}
}
只要一运行代码并在 Flutter 唤出该路由,如果你能看到组件树本该空白的 Texture 区域稳稳当当地显示出了底层 C++ 染出的暗红色背景,或是中心出现了一个不依靠任何 Flutter Canvas API 画出的原生三角形——那么恭喜你,这座连接跨端 UI 与底层 GPU 显卡的桥梁已经彻底竣工。
结语
通过使用 Flutter 内置的 Texture 组件,我们完美剥离了 Android 平台中由于 PlatformView / GLSurfaceView 引申出的不必要开销及层次问题,并且完全在后台线程独立控制了底层的 OpenGL EGL 上下文环境。利用 JNI 将渲染生命周期及事件逐帧映射给了 C++ 层自行封装渲染引擎,我们接下来的重心就可以放在如何在 C++ 层面完成真正的 Shader 开发,以及对 3MF 模型文件内的顶点数据的精确渲染与展现。