特別感謝 RealCoolSnow/live2d-android 大佬的開元分享
- 關(guān)于 Flutter 端加載代碼:其實(shí)只要 Android 原生能正常顯示,F(xiàn)lutter 這邊用 PlatformView 對(duì)接的邏輯很固定,讓 AI 生成基礎(chǔ)代碼就行(比如 Dart 側(cè)的 AndroidView 創(chuàng)建、原生視圖注冊(cè)),不用重復(fù)貼冗余代碼;
- 重點(diǎn)說明:這篇文章會(huì)聚焦「Android 原生集成核心流程」「跨層調(diào)用踩坑」「OpenGL 上下文沖突解決」這些 AI 寫不出來的實(shí)戰(zhàn)細(xì)節(jié),都是我實(shí)測(cè)踩過的硬坑;
- 懇請(qǐng)大家:覺得有用的話,務(wù)必給原作者大佬點(diǎn)個(gè) star??RealCoolSnow/live2d-android,再給這篇文章點(diǎn)個(gè)贊~ 開源作者的分享不易,你的支持才是他們持續(xù)輸出的動(dòng)力,不然以后可就難挖到這么好的免費(fèi)資源啦!
第一章,要在android上集成live2d
Live2D SDK 集成說明文檔
1. 概述
本文檔詳細(xì)說明了在Android Flutter應(yīng)用中如何集成Live2D SDK,實(shí)現(xiàn)從模型加載到顯示的完整流程。整個(gè)集成涉及Java/Kotlin層、JNI層和C++原生層的協(xié)同工作。
2. 架構(gòu)層次
┌─────────────────────────────────────┐
│ Flutter層 (Dart) │
│ - Live2DView Widget │
└──────────────┬──────────────────────┘
│ PlatformView
┌──────────────▼──────────────────────┐
│ Android Kotlin層 │
│ - Live2DPlatformView │
│ - Live2D_v3 (Java接口) │
└──────────────┬──────────────────────┘
│ JNI調(diào)用
┌──────────────▼──────────────────────┐
│ C++ JNI層 │
│ - lapp_model.cpp (JNI綁定) │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ Live2D SDK C++層 │
│ - LAppModel (模型管理) │
│ - CubismRenderer (渲染器) │
│ - CubismModel (模型數(shù)據(jù)) │
└─────────────────────────────────────┘
3. 核心組件說明
3.1 Live2D_v3.java - Java/Kotlin接口層
文件位置: android/app/src/main/kotlin/com/hornhuang/tomato_plan/Live2D_v3.java
這是Live2D SDK的Java/Kotlin接口,提供SDK初始化和模型管理功能。
關(guān)鍵代碼:
public class Live2D_v3 {
private static boolean initialized = false;
private static Context context;
public static void init(Context ctx) {
if (!initialized) {
context = ctx;
Csm_CubismFramework.initialize();
initialized = true;
}
}
public static void dispose() {
if (initialized) {
Csm_CubismFramework.dispose();
initialized = false;
}
}
public static void clearBuffer(float r, float g, float b, float a) {
GLES20.glClearColor(r, g, b, a);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
}
public static class LAppModel {
private final long nativeModel;
public LAppModel() {
nativeModel = createNativeModel();
}
private native long createNativeModel();
private native void destroyNativeModel(long model);
public native void loadModelJson(String fileName);
public native void resize(int ww, int wh);
public native void update();
public native void draw();
public native void startMotion(String group, int no, int priority,
OnBeganMotionCallback onStart, OnFinishedMotionCallback onFinish);
public native void setExpression(String expressionID);
public native void setParameterValue(String id, float value);
public native void setParameterValueByIndex(int index, float value);
public native void addParameterValue(String id, float value);
public native void addParameterValueByIndex(int index, float value);
public native void setPartsOpacity(String id, float opacity);
public native void setPartsOpacityByIndex(int index, float opacity);
public native void hitTest(String hitAreaName, float x, float y);
public native void setDragging(float x, float y);
public native void setAcceleration(float x, float y, float z);
}
}
3.2 lapp_model.cpp - JNI綁定層
文件位置: android/app/src/main/cpp/lapp_model.cpp
負(fù)責(zé)將Java/Kotlin方法調(diào)用轉(zhuǎn)換為C++函數(shù)調(diào)用,實(shí)現(xiàn)跨語言交互。
關(guān)鍵代碼:
extern "C" JNIEXPORT jlong JNICALL
Java_com_hornhuang_tomato_1plan_Live2D_1v3_00024LAppModel_createNativeModel(JNIEnv *, jobject) {
auto *model = new LAppModel();
return reinterpret_cast<long>(model);
}
extern "C" JNIEXPORT void JNICALL
Java_com_hornhuang_tomato_1plan_Live2D_1v3_00024LAppModel_loadModelJson(JNIEnv *env, jobject thiz,
jstring file_name) {
const char *json_file = env->GetStringUTFChars(file_name, nullptr);
getModel(env, thiz)->LoadAssets(json_file);
env->ReleaseStringUTFChars(file_name, json_file);
}
extern "C" JNIEXPORT void JNICALL
Java_com_hornhuang_tomato_1plan_Live2D_1v3_00024LAppModel_resize(JNIEnv *env, jobject thiz,
jint ww, jint wh) {
getModel(env, thiz)->Resize(ww, wh);
}
extern "C" JNIEXPORT void JNICALL
Java_com_hornhuang_tomato_1plan_Live2D_1v3_00024LAppModel_update(JNIEnv *env, jobject thiz) {
getModel(env, thiz)->Update();
}
extern "C" JNIEXPORT void JNICALL
Java_com_hornhuang_tomato_1plan_Live2D_1v3_00024LAppModel_draw(JNIEnv *env, jobject thiz) {
getModel(env, thiz)->Draw();
}
3.3 LAppModel.cpp - 模型管理核心
文件位置: android/app/src/main/cpp/Main/src/LAppModel.cpp
實(shí)現(xiàn)Live2D模型的加載、更新、渲染和交互處理。
關(guān)鍵代碼:
void LAppModel::LoadAssets(const Csm::csmChar* fileName) {
// 解析模型文件路徑
std::filesystem::path p = std::filesystem::u8path(fileName);
_modelHomeDir = p.parent_path().u8string().c_str();
// 讀取并解析.model3.json文件
Csm::csmSizeInt size;
Csm::csmByte* buffer = CreateBuffer(fileName, &size);
_modelSetting = Csm::Model::CubismModelSettingJson::Create(buffer, size);
DeleteBuffer(buffer, fileName);
// 設(shè)置模型
SetupModel(_modelSetting);
// 設(shè)置紋理
SetupTextures();
// 預(yù)加載動(dòng)作
PreloadMotionGroup(MotionGroupIdle);
}
void LAppModel::SetupModel(Csm::Model::ICubismModelSetting* setting) {
// 從模型設(shè)置中加載模型數(shù)據(jù)
_modelJson = setting->GetModelFileName();
std::string modelPath = _modelHomeDir + "/" + _modelJson;
Csm::csmSizeInt size;
Csm::csmByte* buffer = CreateBuffer(modelPath.c_str(), &size);
LoadModel(buffer, size);
DeleteBuffer(buffer, modelPath.c_str());
// 設(shè)置模型參數(shù)
_model->SaveParameters();
// 創(chuàng)建渲染器
_renderer = new Csm::Rendering::CubismRenderer_OpenGLES2();
_renderer->Initialize(_model);
}
void LAppModel::Update() {
const Csm::csmFloat32 deltaTimeSeconds = LAppPal::GetDeltaTime();
_userTimeSeconds += deltaTimeSeconds;
// 更新動(dòng)作
_motionManager->UpdateMotion(_model, deltaTimeSeconds);
// 更新模型參數(shù)
_model->Update();
}
void LAppModel::Draw() {
// 設(shè)置投影矩陣
Csm::CubismMatrix44 projection;
projection.Scale(1.0f, 1.0f);
_renderer->SetMvpMatrix(&projection);
// 渲染模型
_renderer->DrawModel();
}
3.4 Live2DPlatformView.kt - Flutter平臺(tái)視圖
文件位置: android/app/src/main/kotlin/com/hornhuang/tomato_plan/Live2DPlatformView.kt
實(shí)現(xiàn)Flutter的PlatformView,在Flutter中嵌入Live2D渲染。
關(guān)鍵代碼:
class Live2DPlatformView(context: Context, id: Int, creationParams: Map<String, Any>?)
: PlatformView {
private val glSurfaceView = GLSurfaceView(context)
private val renderer = Live2DRenderer(context)
init {
glSurfaceView.setEGLContextClientVersion(2)
glSurfaceView.setRenderer(renderer)
glSurfaceView.renderMode = GLSurfaceView.RENDERMODE_CONTINUOUSLY
}
override fun getView(): View = glSurfaceView
override fun dispose() {
// 清理資源
}
}
class Live2DRenderer(private val context: Context) : GLSurfaceView.Renderer {
lateinit var model: LAppModel
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
// 初始化Live2D SDK
Live2D_v3.init(context)
// 創(chuàng)建模型實(shí)例
model = LAppModel().apply {
// 加載模型文件
loadModelJson("assets://mianfeimox/llny.model3.json")
}
}
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
// 調(diào)整模型尺寸
model.resize(width, height)
}
override fun onDrawFrame(gl: GL10?) {
// 清空緩沖區(qū)
Live2D_v3.clearBuffer(0f, 1f, 0f, 0f)
// 更新模型狀態(tài)
model.update()
// 設(shè)置參數(shù)(例如眨眼)
model.setParameterValue("Param14", 1f)
// 繪制模型
model.draw()
}
}
4. 完整流程說明
4.1 初始化流程
1. Flutter啟動(dòng)
↓
2. 創(chuàng)建Live2DPlatformView
↓
3. GLSurfaceView創(chuàng)建OpenGL ES 2.0上下文
↓
4. onSurfaceCreated回調(diào)觸發(fā)
↓
5. 調(diào)用Live2D_v3.init(context)
↓
6. Csm_CubismFramework.initialize() 初始化Live2D框架
↓
7. 創(chuàng)建LAppModel實(shí)例
↓
8. 調(diào)用loadModelJson加載模型
4.2 模型加載流程
1. loadModelJson("assets://mianfeimox/llny.model3.json")
↓
2. JNI調(diào)用: Java_com_hornhuang_tomato_1plan_Live2D_1v3_00024LAppModel_loadModelJson
↓
3. LAppModel::LoadAssets(fileName)
↓
4. 讀取.model3.json文件
↓
5. 解析JSON獲取模型文件路徑、紋理、動(dòng)作等信息
↓
6. SetupModel() - 加載.moc3模型文件
↓
7. SetupTextures() - 加載紋理圖片
↓
8. PreloadMotionGroup() - 預(yù)加載動(dòng)作文件
↓
9. 創(chuàng)建CubismRenderer并初始化
4.3 渲染循環(huán)流程
每一幀:
1. onDrawFrame回調(diào)觸發(fā)
↓
2. glClear清空顏色緩沖區(qū)
↓
3. model.update()
↓
4. _motionManager->UpdateMotion() - 更新動(dòng)作狀態(tài)
↓
5. _model->Update() - 更新模型參數(shù)
↓
6. model.draw()
↓
7. 設(shè)置投影矩陣
↓
8. _renderer->DrawModel() - 渲染模型
↓
9. OpenGL繪制到屏幕
4.4 交互處理流程
用戶觸摸:
1. 觸摸事件傳遞到Live2DPlatformView
↓
2. 計(jì)算觸摸坐標(biāo)
↓
3. model.hitTest(hitAreaName, x, y)
↓
4. 檢測(cè)是否擊中可交互區(qū)域
↓
5. 如果擊中,觸發(fā)相應(yīng)動(dòng)作:
- model.startMotion() - 播放動(dòng)作
- model.setExpression() - 設(shè)置表情
- model.setParameterValue() - 設(shè)置參數(shù)
5. 關(guān)鍵技術(shù)點(diǎn)
5.1 OpenGL上下文管理
每個(gè)OpenGL上下文需要獨(dú)立初始化Live2D SDK:
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
// 每個(gè)OpenGL上下文都需要單獨(dú)初始化
Live2D_v3.init(context)
model = LAppModel()
}
5.2 資源路徑處理
Android assets路徑需要轉(zhuǎn)換為C++可訪問的路徑:
void LAppModel::LoadAssets(const Csm::csmChar* fileName) {
// assets://路徑轉(zhuǎn)換為實(shí)際文件路徑
std::filesystem::path p = std::filesystem::u8path(fileName);
_modelHomeDir = p.parent_path().u8string().c_str();
}
5.3 動(dòng)作管理
動(dòng)作通過優(yōu)先級(jí)系統(tǒng)管理:
void LAppModel::StartMotion(const Csm::csmChar* group, Csm::csmInt32 no,
Csm::csmInt32 priority,
Csm::FinishedMotionCallback onFinishedMotionHandler) {
if (priority == _priority) {
_motionManager->SetReservePriority(priority);
} else if (priority < _motionManager->GetReservePriority()) {
return;
}
_motionManager->StartMotionPriority(
_model,
group,
no,
priority,
onFinishedMotionHandler
);
}
5.4 參數(shù)控制
Live2D模型參數(shù)實(shí)時(shí)控制:
// 設(shè)置參數(shù)值
model.setParameterValue("ParamEyeLOpen", 0.5f)
// 增加參數(shù)值
model.addParameterValue("ParamAngleX", 0.1f)
// 設(shè)置部件透明度
model.setPartsOpacity("PartArmL", 0.8f)
6. 常見問題與解決方案
6.1 OpenGL上下文錯(cuò)誤
問題: GL_INVALID_VALUE (0x501)錯(cuò)誤
原因: 在不同的OpenGL上下文中使用同一個(gè)Live2D實(shí)例
解決方案: 每個(gè)OpenGL上下文獨(dú)立初始化Live2D SDK和模型實(shí)例
6.2 模型加載失敗
問題: 模型無法顯示或崩潰
檢查項(xiàng):
- 模型文件路徑是否正確
- .model3.json文件格式是否正確
- 紋理文件是否存在
- 動(dòng)作文件是否完整
6.3 性能優(yōu)化
建議:
- 使用RENDERMODE_WHEN_DIRTY而非RENDERMODE_CONTINUOUSLY
- 預(yù)加載常用動(dòng)作
- 合理設(shè)置動(dòng)作優(yōu)先級(jí)
- 避免頻繁創(chuàng)建銷毀模型實(shí)例
7. 文件結(jié)構(gòu)
android/
├── app/
│ ├── src/
│ │ ├── main/
│ │ │ ├── kotlin/com/hornhuang/tomato_plan/
│ │ │ │ ├── Live2D_v3.java # Java/Kotlin接口
│ │ │ │ ├── Live2DPlatformView.kt # Flutter平臺(tái)視圖
│ │ │ │ └── MainActivity.kt # 主Activity
│ │ │ ├── cpp/
│ │ │ │ ├── lapp_model.cpp # JNI綁定
│ │ │ │ ├── live2d.cpp # Live2D初始化
│ │ │ │ └── Main/src/
│ │ │ │ └── LAppModel.cpp # 模型管理
│ │ │ └── assets/
│ │ │ └── mianfeimox/ # Live2D模型資源
│ │ │ ├── llny.model3.json
│ │ │ ├── llny.moc3
│ │ │ ├── textures/
│ │ │ └── motions/
8. 擴(kuò)展功能
8.1 添加交互事件
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
model.hitTest("Head", event.x, event.y)
}
MotionEvent.ACTION_MOVE -> {
model.setDragging(event.x, event.y)
}
MotionEvent.ACTION_UP -> {
model.setDragging(0f, 0f)
}
}
return true
}
8.2 播放動(dòng)作
// 播放指定動(dòng)作
model.startMotion("TapBody", 0, 3,
{ group, no -> println("動(dòng)作開始: $group, $no") },
{ self -> println("動(dòng)作結(jié)束") }
)
// 設(shè)置表情
model.setExpression("f01")
8.3 物理模擬
// 設(shè)置加速度(用于物理模擬)
model.setAcceleration(0.5f, 0.0f, 0.0f)
9. 總結(jié)
Live2D SDK在Android Flutter應(yīng)用中的集成涉及多個(gè)層次的協(xié)作:
- Flutter層: 通過PlatformView嵌入原生視圖
- Java/Kotlin層: 提供Live2D SDK的接口封裝
- JNI層: 實(shí)現(xiàn)Java與C++的跨語言調(diào)用
- C++層: 實(shí)現(xiàn)Live2D模型的核心功能
關(guān)鍵要點(diǎn):
- 每個(gè)OpenGL上下文需要獨(dú)立初始化Live2D SDK
- 模型加載遵循: JSON解析 → 資源加載 → 渲染器初始化
- 渲染循環(huán): 更新動(dòng)作 → 更新模型 → 繪制
- 交互通過參數(shù)控制和動(dòng)作播放實(shí)現(xiàn)
通過合理管理OpenGL上下文和資源生命周期,可以實(shí)現(xiàn)穩(wěn)定高效的Live2D模型渲染效果。
第二章 Flutter側(cè)的渲染與展示
Flutter調(diào)用Android Native組件展示Live2D - 問題解決總結(jié)
1. Flutter調(diào)用Android Native組件展示Live2D的完整流程
1.1 整體架構(gòu)
Flutter層 (Dart)
↓ PlatformView
Android Kotlin層
↓ JNI調(diào)用
C++ Native層
↓ Live2D SDK
OpenGL ES渲染
1.2 詳細(xì)調(diào)用鏈路
第一步:Flutter端創(chuàng)建PlatformView
// 在Flutter代碼中創(chuàng)建Live2DView
AndroidView(
viewType: 'com.hornhuang.tomato_plan/live2d_view',
creationParams: <String, dynamic>{},
creationParamsCodec: const StandardMessageCodec(),
)
第二步:MainActivity注冊(cè)PlatformView
// MainActivity.kt
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
flutterEngine
.platformViewsController
.registry
.registerViewFactory(
"com.hornhuang.tomato_plan/live2d_view",
Live2DPlatformViewFactory()
)
}
}
第三步:Live2DPlatformViewFactory創(chuàng)建PlatformView
class Live2DPlatformViewFactory : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
override fun create(context: Context, viewId: Int, args: Any?): PlatformView {
return Live2DPlatformView(context, viewId, args as Map<String, Any>?)
}
}
第四步:Live2DPlatformView創(chuàng)建GLSurfaceView和渲染器
class Live2DPlatformView(context: Context, id: Int, creationParams: Map<String, Any>?)
: PlatformView {
private val glSurfaceView = GLSurfaceView(context)
private val renderer = Live2DRenderer(context)
init {
glSurfaceView.setEGLContextClientVersion(2)
glSurfaceView.setRenderer(renderer)
glSurfaceView.renderMode = GLSurfaceView.RENDERMODE_CONTINUOUSLY
}
override fun getView(): View = glSurfaceView
}
第五步:Live2DRenderer初始化Live2D SDK
class Live2DRenderer(private val context: Context) : GLSurfaceView.Renderer {
lateinit var model: LAppModel
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
// 初始化Live2D SDK
Live2D_v3.init(context)
// 創(chuàng)建模型實(shí)例
model = LAppModel().apply {
loadModelJson("assets://mianfeimox/llny.model3.json")
}
}
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
model.resize(width, height)
}
override fun onDrawFrame(gl: GL10?) {
Live2D_v3.clearBuffer(0f, 1f, 0f, 0f)
model.update()
model.setParameterValue("Param14", 1f)
model.draw()
}
}
第六步:JNI調(diào)用C++層
// Live2D_v3.java
public class LAppModel {
private final long nativeModel;
public native void loadModelJson(String fileName);
public native void update();
public native void draw();
}
// lapp_model.cpp
extern "C" JNIEXPORT void JNICALL
Java_com_hornhuang_tomato_1plan_Live2D_1v3_00024LAppModel_loadModelJson(JNIEnv *env, jobject thiz,
jstring file_name) {
const char *json_file = env->GetStringUTFChars(file_name, nullptr);
getModel(env, thiz)->LoadAssets(json_file);
env->ReleaseStringUTFChars(file_name, json_file);
}
extern "C" JNIEXPORT void JNICALL
Java_com_hornhuang_tomato_1plan_Live2D_1v3_00024LAppModel_draw(JNIEnv *env, jobject thiz) {
getModel(env, thiz)->Draw();
}
第七步:C++層加載和渲染Live2D模型
// LAppModel.cpp
void LAppModel::LoadAssets(const Csm::csmChar* fileName) {
// 解析.model3.json文件
Csm::csmByte* buffer = CreateBuffer(fileName, &size);
_modelSetting = Csm::Model::CubismModelSettingJson::Create(buffer, size);
DeleteBuffer(buffer, fileName);
// 設(shè)置模型和紋理
SetupModel(_modelSetting);
SetupTextures();
PreloadMotionGroup(MotionGroupIdle);
}
void LAppModel::Draw() {
Csm::CubismMatrix44 projection;
projection.Scale(1.0f, 1.0f);
_renderer->SetMvpMatrix(&projection);
_renderer->DrawModel();
}
2. 遇到的問題和Bug
問題1:Platform View未注冊(cè)錯(cuò)誤
錯(cuò)誤信息
Trying to create a platform view of unregistered type: com.hornhuang.tomato_plan/live2d_view
觸發(fā)原因
刪除了NativeTextView相關(guān)文件后,MainActivity.kt中仍然保留著NativeTextViewFactory的注冊(cè)代碼,但沒有注冊(cè)Live2DPlatformViewFactory。
解決方案
- 刪除MainActivity.kt中的NativeTextViewFactory注冊(cè)代碼
- 添加Live2DPlatformViewFactory的注冊(cè)
// MainActivity.kt
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
flutterEngine
.platformViewsController
.registry
.registerViewFactory(
"com.hornhuang.tomato_plan/live2d_view",
Live2DPlatformViewFactory()
)
}
}
問題2:編譯錯(cuò)誤 - 未解析的引用
錯(cuò)誤信息
Unresolved reference: LAppModel
Unresolved reference: drag
Unresolved reference: touch
Unresolved reference: release
觸發(fā)原因
Live2DPlatformView.kt中使用了LAppModel類,但沒有導(dǎo)入正確的包。
解決方案
在Live2DPlatformView.kt文件頂部添加導(dǎo)入語句:
import com.hornhuang.tomato_plan.Live2D_v3.LAppModel
問題3:Live2DActivity能顯示,但Flutter的Live2DView顯示空白(核心問題)
現(xiàn)象描述
- 打開Live2DActivity:Live2D模型正常顯示
- 返回Flutter首頁:Live2DView顯示一片空白(綠色背景)
- 日志中出現(xiàn)OpenGL錯(cuò)誤:
glGetError() returned error 0x501
觸發(fā)原因分析
根本原因:OpenGL上下文沖突
-
Live2DActivity和Live2DPlatformView使用不同的OpenGL上下文
- Live2DActivity有自己的GLSurfaceView,創(chuàng)建獨(dú)立的OpenGL上下文
- Live2DPlatformView也有自己的GLSurfaceView,創(chuàng)建另一個(gè)獨(dú)立的OpenGL上下文
-
Live2D SDK的初始化問題
- 最初的實(shí)現(xiàn)中,Live2D_v3.init()在MainActivity中只調(diào)用一次
- 這導(dǎo)致Live2D SDK的靜態(tài)資源(如著色器程序)在第一個(gè)OpenGL上下文中創(chuàng)建
- 當(dāng)切換到第二個(gè)OpenGL上下文時(shí),這些資源無效
-
著色器程序的OpenGL上下文綁定
- OpenGL的著色器程序是上下文綁定的
- 在上下文A中創(chuàng)建的著色器程序在上下文B中無法使用
- CubismRenderer在創(chuàng)建時(shí)會(huì)編譯和鏈接著色器程序
- 這些程序只在創(chuàng)建它們的上下文中有效
-
錯(cuò)誤流程
用戶打開Live2DActivity ↓ Live2DActivity的OpenGL上下文創(chuàng)建 ↓ Live2D_v3.init() 在MainActivity中調(diào)用(第一次) ↓ 著色器程序在Live2DActivity的上下文中創(chuàng)建 ↓ Live2D模型正常顯示 用戶返回Flutter首頁 ↓ Live2DPlatformView的OpenGL上下文創(chuàng)建 ↓ Live2D_v3.init() 檢測(cè)到已初始化,跳過 ↓ Live2DPlatformView嘗試使用Live2D SDK ↓ 嘗試使用在Live2DActivity上下文中創(chuàng)建的著色器程序 ↓ OpenGL錯(cuò)誤 0x501 (GL_INVALID_VALUE) ↓ 渲染失敗,顯示空白
解決方案
方案:每個(gè)OpenGL上下文獨(dú)立初始化Live2D SDK
- 修改Live2D_v3.java,支持多次初始化
public class Live2D_v3 {
private static boolean initialized = false;
private static Context context;
public static void init(Context ctx) {
if (!initialized) {
context = ctx;
Csm_CubismFramework.initialize();
initialized = true;
}
}
}
- 修改Live2DPlatformView.kt,在onSurfaceCreated中初始化
class Live2DRenderer(private val context: Context) : GLSurfaceView.Renderer {
lateinit var model: LAppModel
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
// 在每個(gè)OpenGL上下文中獨(dú)立初始化Live2D SDK
Live2D_v3.init(context)
// 創(chuàng)建模型實(shí)例
model = LAppModel().apply {
loadModelJson("assets://mianfeimox/llny.model3.json")
}
}
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
model.resize(width, height)
}
override fun onDrawFrame(gl: GL10?) {
Live2D_v3.clearBuffer(0f, 1f, 0f, 0f)
model.update()
model.setParameterValue("Param14", 1f)
model.draw()
}
}
- 修改Live2DActivity.kt,在MyRenderer中初始化
class Live2DActivity : AppCompatActivity() {
private lateinit var glSurfaceView: GLSurfaceView
private lateinit var renderer: MyRenderer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_live2d)
glSurfaceView = findViewById(R.id.glSurfaceView)
renderer = MyRenderer(this)
glSurfaceView.setEGLContextClientVersion(2)
glSurfaceView.setRenderer(renderer)
}
}
class MyRenderer(private val context: Context) : GLSurfaceView.Renderer {
private lateinit var model: LAppModel
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
// 在Live2DActivity的OpenGL上下文中初始化Live2D SDK
Live2D_v3.init(context)
model = LAppModel().apply {
loadModelJson("assets://mianfeimox/llny.model3.json")
}
}
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
model.resize(width, height)
}
override fun onDrawFrame(gl: GL10?) {
Live2D_v3.clearBuffer(0f, 0f, 0f, 0f)
model.update()
model.setParameterValue("Param14", 1f)
model.draw()
}
}
- 移除MainActivity中的集中初始化
// MainActivity.kt - 不再在這里初始化Live2D
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
flutterEngine
.platformViewsController
.registry
.registerViewFactory(
"com.hornhuang.tomato_plan/live2d_view",
Live2DPlatformViewFactory()
)
}
}
修復(fù)后的流程
用戶打開Live2DActivity
↓
Live2DActivity的OpenGL上下文創(chuàng)建
↓
MyRenderer.onSurfaceCreated() 調(diào)用
↓
Live2D_v3.init() 第一次初始化
↓
著色器程序在Live2DActivity的上下文中創(chuàng)建
↓
Live2D模型正常顯示
用戶返回Flutter首頁
↓
Live2DPlatformView的OpenGL上下文創(chuàng)建
↓
Live2DRenderer.onSurfaceCreated() 調(diào)用
↓
Live2D_v3.init() 檢測(cè)到已初始化,跳過框架初始化
↓
創(chuàng)建新的LAppModel實(shí)例
↓
CubismRenderer在Live2DPlatformView的上下文中創(chuàng)建新的著色器程序
↓
Live2D模型正常顯示
3. 關(guān)鍵技術(shù)點(diǎn)總結(jié)
3.1 OpenGL上下文隔離
- 每個(gè)GLSurfaceView創(chuàng)建獨(dú)立的OpenGL上下文
- OpenGL資源(著色器程序、紋理等)是上下文綁定的
- 不同上下文之間不能共享OpenGL資源
3.2 Live2D SDK的初始化策略
- 框架初始化(Csm_CubismFramework.initialize()):只需一次
- 渲染器初始化(CubismRenderer):每個(gè)OpenGL上下文需要獨(dú)立創(chuàng)建
- 模型實(shí)例:每個(gè)OpenGL上下文需要獨(dú)立的LAppModel實(shí)例
3.3 Flutter PlatformView的生命周期
- PlatformView創(chuàng)建時(shí),GLSurfaceView也會(huì)創(chuàng)建
- GLSurfaceView的onSurfaceCreated在OpenGL上下文創(chuàng)建時(shí)調(diào)用
- onSurfaceChanged在視圖尺寸變化時(shí)調(diào)用
- onDrawFrame在每一幀渲染時(shí)調(diào)用
4. 問題排查過程
4.1 初步排查
- 檢查Live2DActivity能正常顯示,說明模型文件和Live2D SDK本身沒有問題
- 檢查Flutter的Live2DView顯示綠色背景,說明GLSurfaceView正常工作
- 日志中出現(xiàn)OpenGL錯(cuò)誤,指向渲染問題
4.2 深入分析
- 分析Live2DActivity和Live2DPlatformView的代碼差異
- 發(fā)現(xiàn)兩者都使用Live2D_v3.init(),但調(diào)用位置不同
- 研究OpenGL上下文的管理機(jī)制
- 確認(rèn)著色器程序的上下文綁定特性
4.3 驗(yàn)證假設(shè)
- 在Live2DPlatformView的onSurfaceCreated中添加日志
- 觀察Live2D_v3.init()的調(diào)用時(shí)機(jī)
- 確認(rèn)OpenGL錯(cuò)誤的觸發(fā)時(shí)機(jī)
- 驗(yàn)證獨(dú)立初始化方案的有效性
5. 經(jīng)驗(yàn)教訓(xùn)
5.1 OpenGL上下文管理
- 在Android中使用多個(gè)GLSurfaceView時(shí),要注意上下文隔離
- OpenGL資源不能跨上下文共享
- 每個(gè)上下文需要獨(dú)立初始化和清理資源
5.2 Live2D SDK集成
- Live2D SDK的框架初始化可以全局進(jìn)行
- 但渲染相關(guān)的資源(著色器程序、紋理等)需要每個(gè)上下文獨(dú)立創(chuàng)建
- 模型實(shí)例也應(yīng)該每個(gè)上下文獨(dú)立創(chuàng)建
5.3 Flutter PlatformView開發(fā)
- PlatformView的生命周期與原生View一致
- 需要正確處理OpenGL上下文的創(chuàng)建和銷毀
- 避免在PlatformView中使用靜態(tài)的OpenGL資源
6. 最佳實(shí)踐
6.1 初始化模式
// 推薦的初始化模式
class MyRenderer(private val context: Context) : GLSurfaceView.Renderer {
private lateinit var model: LAppModel
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
// 每個(gè)OpenGL上下文獨(dú)立初始化
Live2D_v3.init(context)
// 創(chuàng)建獨(dú)立的模型實(shí)例
model = LAppModel()
model.loadModelJson("assets://model/xxx.model3.json")
}
override fun onSurfaceDestroyed(gl: GL10?) {
// 清理資源
// 注意:Live2D_v3.dispose()不應(yīng)該在這里調(diào)用
// 因?yàn)榭赡苓€有其他OpenGL上下文在使用
}
}
6.2 資源管理
- 避免在多個(gè)OpenGL上下文之間共享OpenGL資源
- 每個(gè)上下文獨(dú)立創(chuàng)建和管理自己的資源
- 注意資源的生命周期,避免內(nèi)存泄漏
6.3 錯(cuò)誤處理
- 使用glGetError()檢測(cè)OpenGL錯(cuò)誤
- 記錄詳細(xì)的日志以便排查問題
- 理解OpenGL錯(cuò)誤碼的含義(如0x501 = GL_INVALID_VALUE)
7. 總結(jié)
通過這次問題排查和修復(fù),我們深入理解了:
- Flutter PlatformView的工作原理
- OpenGL上下文的管理機(jī)制
- Live2D SDK的正確集成方式
- 跨上下文資源共享的限制
核心解決方案是:每個(gè)OpenGL上下文獨(dú)立初始化Live2D相關(guān)的渲染資源,確保著色器程序等OpenGL資源在正確的上下文中創(chuàng)建和使用。
這個(gè)經(jīng)驗(yàn)對(duì)于其他需要集成OpenGL渲染的Flutter項(xiàng)目也具有參考價(jià)值。