Skip to content

Instantly share code, notes, and snippets.

@chaosgoo
Created March 26, 2026 04:40
Show Gist options
  • Select an option

  • Save chaosgoo/dd26cbe819852957e3c9d4bd8e7f0900 to your computer and use it in GitHub Desktop.

Select an option

Save chaosgoo/dd26cbe819852957e3c9d4bd8e7f0900 to your computer and use it in GitHub Desktop.
3mf renderer in flutter
package com.chaosgoo.metasequoia.egl
import android.opengl.EGL14
import android.opengl.EGLConfig
import android.util.Log
import android.view.Surface
/**
* Kotlin层的EGL核心类,负责管理EGL上下文、EGL显示和EGL表面等资源。
* EGL = Embedded Graphic Library
*/
class EglCore(surface: Surface) {
private val TAG = this::class.java.simpleName
private var eglDisplay = EGL14.EGL_NO_DISPLAY
private var eglContext = EGL14.EGL_NO_CONTEXT
private var eglSurface = EGL14.EGL_NO_SURFACE
init {
eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
val version = IntArray(2)
EGL14.eglInitialize(eglDisplay, version, 0, version, 1)
Log.i(TAG, "EGL version: ${version[0]}.${version[1]}")
val configAttribs = intArrayOf(
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
EGL14.EGL_RED_SIZE, 8,
EGL14.EGL_GREEN_SIZE, 8,
EGL14.EGL_BLUE_SIZE, 8,
EGL14.EGL_DEPTH_SIZE, 16,
EGL14.EGL_NONE
)
val configs = arrayOfNulls<EGLConfig>(1)
val numConfigs = IntArray(1)
EGL14.eglChooseConfig(eglDisplay, configAttribs, 0, configs, 0, 1, numConfigs, 0)
eglContext = EGL14.eglCreateContext(
eglDisplay,
configs[0],
EGL14.EGL_NO_CONTEXT,
intArrayOf(EGL14.EGL_CONTEXT_CLIENT_VERSION, 3, EGL14.EGL_NONE),
0
)
val surfaceAttribs = intArrayOf(EGL14.EGL_NONE)
eglSurface =
EGL14.eglCreateWindowSurface(eglDisplay, configs[0], surface, surfaceAttribs, 0)
}
fun makeCurrent() = EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)
fun swapBuffer() = EGL14.eglSwapBuffers(eglDisplay, eglSurface)
fun release() {
EGL14.eglDestroyContext(eglDisplay, eglContext)
EGL14.eglDestroySurface(eglDisplay, eglSurface)
eglDisplay = EGL14.EGL_NO_DISPLAY
eglContext = EGL14.EGL_NO_CONTEXT
eglSurface = EGL14.EGL_NO_SURFACE
}
}
package com.chaosgoo.metasequoia.texture
import android.opengl.GLES30
import android.os.Handler
import android.os.HandlerThread
import android.os.Message
import android.util.Log
import android.view.Choreographer
import android.view.Surface
import com.chaosgoo.metasequoia.egl.EglCore
import com.chaosgoo.metasequoia.renderer.NativeDrawerRenderer
import io.flutter.view.TextureRegistry
class GLTextureHandler2(
private val registry: TextureRegistry,
private val shaderParams: Map<String, String>
) {
private val TAG = this::class.java.simpleName
private val entry = registry.createSurfaceTexture()
private val surfaceTexture = entry.surfaceTexture()
private var surface: Surface? = null
private var eglCore: EglCore? = null
private var render: NativeDrawerRenderer? = null
private var isRendering = false
private val choreographer = Choreographer.getInstance()
private val renderThread = HandlerThread("RenderThread").apply { start() }
var renderMode = GLES30.GL_TRIANGLES
var rotateX = 0.0
var rotateY = 0.0
val INIT = 0
val DRAW = 1
val STOP = 2
val UPDATE_SIZE = 3
val LOAD_MODEL = 4
val ROTATE = 5
val cb = Handler.Callback { msg ->
when (msg.what) {
INIT -> {
Log.i(TAG, "INIT CMD")
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()
}
DRAW -> {
render?.onDrawFrame(null)
eglCore?.swapBuffer()
}
STOP -> {
}
UPDATE_SIZE -> {
Log.i(TAG, "UPDATE_SIZE CMD")
val (width, height) = msg.obj as? Pair<Int, Int> ?: (0 to 0)
surfaceTexture.setDefaultBufferSize(width, height)
render?.onSurfaceChanged(null, width, height)
}
LOAD_MODEL -> {
Log.i(TAG, "LOAD_MODEL CMD")
val modelPath = msg.obj as? String ?: ""
if (render !is NativeDrawerRenderer) return@Callback true
(render as? NativeDrawerRenderer)?.loadModel(modelPath)
}
ROTATE -> {
render?.rotate(rotateX.toFloat(), rotateY.toFloat())
}
}
return@Callback true
}
private val renderThreadHandler = Handler(renderThread.looper, cb)
private val frameCallback = object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
if (!isRendering) {
Log.d(TAG, "return")
return
}
renderThreadHandler.sendEmptyMessage(DRAW)
choreographer.postFrameCallback(this)
}
}
fun getTextureId() = entry.id()
fun setup(width: Int, height: Int) {
Log.d(TAG, "setup")
surfaceTexture.setDefaultBufferSize(width, height)
renderThreadHandler.sendMessage(
Message().apply {
this.what = INIT
this.obj = width to height
}
)
}
fun updateTextureSize(width: Int, height: Int) {
Log.d(TAG, "updateTextureSize")
surfaceTexture.setDefaultBufferSize(width, height)
renderThreadHandler.sendMessage(
Message().apply {
this.what = UPDATE_SIZE
this.obj = width to height
}
)
}
fun start() {
Log.d(TAG, "start")
if (isRendering) {
Log.d(TAG, "return")
return
}
isRendering = true
choreographer.postFrameCallback(frameCallback)
}
fun stop() {
isRendering = false
choreographer.removeFrameCallback(frameCallback)
}
fun release() {
stop()
entry.release()
surface?.release()
eglCore?.release()
}
fun loadModel(modelPath: String) {
renderThreadHandler.sendMessage(
Message().apply {
this.what = LOAD_MODEL
this.obj = modelPath
}
)
}
fun rotate(x: Float, y: Float) {
rotateX = x.toDouble()
rotateY = y.toDouble()
renderThreadHandler.sendMessage(
Message().apply {
this.what = ROTATE
}
)
}
}
package com.chaosgoo.metasequoia.texture
import android.util.Log
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodChannel
import io.flutter.view.TextureRegistry
class GLTexturePlugin2 : FlutterPlugin {
private val TAG = this::class.java.simpleName
private lateinit var channel: MethodChannel
private lateinit var textureRegistry: TextureRegistry
private lateinit var messenger: BinaryMessenger
private val textureHandlers = mutableMapOf<Long, GLTextureHandler2>()
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
textureRegistry = binding.textureRegistry
messenger = binding.binaryMessenger
channel = MethodChannel(messenger, "com.chaosgoo.metasequoia/gl_texture2")
channel.setMethodCallHandler { call, result ->
when (call.method) {
"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)
}
"changeRenderMode" -> {
// val textureId = call.argument<Int>("textureId") ?: return@setMethodCallHandler
// val renderMode = call.argument<String>("renderMode") ?: "GL_TRIANGLES"
// Log.i(TAG, "changeRenderMode to $renderMode")
// textureHandlers[textureId.toLong()]?.run {
// this.renderMode = when (renderMode) {
// "GL_TRIANGLES" -> {
// GLES30.GL_TRIANGLES
// }
// "GL_LINES" -> {
// GLES30.GL_LINES
// }
// "GL_POINTS" -> {
// GLES30.GL_POINTS
// }
// else -> {
// GLES30.GL_TRIANGLES
// }
// }
// }
}
"loadModel"->{
val textureId = call.argument<Int>("textureId") ?: return@setMethodCallHandler
val modelPath = call.argument<String>("modelPath") ?: return@setMethodCallHandler
Log.i(TAG, "loadModel to $modelPath")
textureHandlers[textureId.toLong()]?.loadModel(modelPath)
}
"updateTextureSize"->{
val textureId = call.argument<Int>("textureId") ?: return@setMethodCallHandler
val width = call.argument<Int>("width") ?: 640
val height = call.argument<Int>("height") ?: 640
Log.i(TAG, "updateTextureSize to $width x $height")
textureHandlers[textureId.toLong()]?.updateTextureSize(width, height)
}
"dispose" -> {
val id = call.argument<Long>("id") ?: return@setMethodCallHandler
textureHandlers[id]?.release()
textureHandlers.remove(id)
result.success(null)
}
"rotate"->{
val textureId = call.argument<Int>("textureId") ?: return@setMethodCallHandler
textureHandlers[textureId.toLong()]?.run {
val rotateX = call.argument<Double>("angleX") ?: 0.0
val rotateY = call.argument<Double>("angleY") ?: 0.0
rotate(rotateX.toFloat(), rotateY.toFloat())
}
}
else ->
result.notImplemented()
}
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
textureHandlers.values.forEach { handler -> handler.release() }
textureHandlers.clear()
}
}
package com.chaosgoo.metasequoia
import android.content.Context
import android.opengl.GLSurfaceView
import android.view.SurfaceHolder
import android.view.View
class LifecycleAwareGLSurfaceView(context: Context) : GLSurfaceView(context) {
override fun onWindowVisibilityChanged(visibility: Int) {
super.onWindowVisibilityChanged(visibility)
if (visibility == View.VISIBLE) {
onResume()
} else {
onPause()
}
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
onPause()
super.surfaceDestroyed(holder)
}
override fun surfaceCreated(holder: SurfaceHolder) {
super.surfaceCreated(holder)
onResume()
}
}
package com.chaosgoo.metasequoia
import com.chaosgoo.metasequoia.base.NativeAndroidViewPlugin
import com.chaosgoo.metasequoia.glsurfaceview.NativeGLSurfaceViewPlugin
import com.chaosgoo.metasequoia.texture.GLTexturePlugin
import com.chaosgoo.metasequoia.texture.GLTexturePlugin2
import com.example.native_lib3mf.NativeAssetManager
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
NativeAssetManager.initAssetManager(context.assets)
super.configureFlutterEngine(flutterEngine)
flutterEngine.plugins.add(GLTexturePlugin())
flutterEngine.plugins.add(GLTexturePlugin2())
}
}
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class ModelFilePickerRoute extends StatefulWidget {
const ModelFilePickerRoute({super.key});
@override
State<ModelFilePickerRoute> createState() => _ModelFilePickerRouteState();
}
class _ModelFilePickerRouteState extends State<ModelFilePickerRoute> {
int? _textureId;
String? modelPath;
double _angleX = 0;
double _angleY = 0;
@override
void initState() {
super.initState();
_setupNativeTexture();
}
Future<void> _setupNativeTexture() async {
final id = await MethodChannel(
"com.chaosgoo.metasequoia/gl_texture2",
).invokeMethod('createGLTexture', {'width': 512, 'height': 512});
setState(() {
_textureId = id;
});
}
_updateNativeTexture(int width, int height) {
if (_textureId != null) {
MethodChannel("com.chaosgoo.metasequoia/gl_texture2").invokeMethod(
'updateTextureSize',
{'textureId': _textureId, 'width': width, 'height': height},
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('选择模型文件')),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Center(
child: FilledButton(
onPressed: () async {
FilePickerResult? result = await FilePicker.platform
.pickFiles();
if (result != null) {
File file = File(result.files.single.path!);
String filePath = file.path;
print("Selected file: $filePath");
if (_textureId != null) {
MethodChannel(
"com.chaosgoo.metasequoia/gl_texture2",
).invokeMethod('loadModel', {
'textureId': _textureId,
'modelPath': filePath,
});
}
setState(() {
modelPath = filePath;
});
} else {
// User canceled the picker
}
},
child: const Text("选择模型文件"),
),
),
Expanded(
child: (_textureId == null || modelPath == null)
? Center(child: CircularProgressIndicator())
: 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!),
);
},
),
),
],
),
);
}
}
package com.chaosgoo.metasequoia.renderer
import android.opengl.GLSurfaceView
import com.example.native_lib3mf.NativeRenderBridge
import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10
class NativeDrawerRenderer() : GLSurfaceView.Renderer {
override fun onDrawFrame(gl: GL10?) {
NativeRenderBridge.onDrawFrame(gl)
}
override fun onSurfaceChanged(
gl: GL10?,
width: Int,
height: Int
) {
NativeRenderBridge.onSurfaceChange(gl, width, height)
}
override fun onSurfaceCreated(
gl: GL10?,
config: EGLConfig?
) {
NativeRenderBridge.onSurfaceCreated(gl, config)
}
fun loadModel(modelPath: String) {
NativeRenderBridge.loadModel(modelPath)
}
fun rotate(x: Float, y: Float) {
NativeRenderBridge.rotateModel(x, y)
}
}
package com.example.native_lib3mf;
import androidx.annotation.Nullable;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
public class NativeRenderBridge {
static {
System.loadLibrary("native_lib3mf");
}
public static native void loadModel(String path);
public static native void rotateModel(float x, float y);
public static native void onDrawFrame(GL10 gl);
public static native void onSurfaceChange(@Nullable GL10 gl, int width, int height);
public static native void onSurfaceCreated(@Nullable GL10 gl, @Nullable EGLConfig config);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment