资讯详情

Live2D Android移植实战:从CMake编译到GLSurfaceView渲染

📅 2026/10/14 16:20:49 | 华诺云谱 👁 阅读
Live2D Android移植实战:从CMake编译到GLSurfaceView渲染
简介面向需要在Android平台集成Live2D动画效果的移动开发者这份编译成功的工程demo提供了从官方源码移植到Android的完整参考。资源包内含C核心库与头文件、Java桥接层、Gradle构建脚本及可直接安装运行的APK能够帮助具备Android开发基础、正在探索2D角色动态渲染的工程师节省自行摸索的时间。压缩包共208个文件以hpp/cpp源码、o/a静态库、class/java等编译产物为主另含XML资源、PNG素材和配置文件整体体积27.06MB。已有585人学习下载。通过剖析工程结构可直观掌握Live2D在Android端的依赖配置、JNI调用写法与编译参数同时打包好的demo可用于真机或模拟器验证对比官方示例快速定位差异。对希望深入定制角色动画、优化渲染性能的开发者而言这份资料也提供了清晰的底包与可扩展方向是接入Live2D时值得参考的实战样例。1. 把 live2d 移植到 android 编译成功 demo卡点往往不在模型而在编译链把 live2d 移植到 android 编译成功 demo听起来像是把 C 源码拖进 Android Studio 就能跑实际上大多数人在链接阶段就折了。真正的问题不在模型而在你要同时处理三套坐标模型本地坐标、OpenGL 视口坐标、Android 触摸坐标外加一堆静态库的 ABI 匹配。本文想分享的是一条我反复走过多次的路怎么把 Live2D 的 Native 框架编进 Android 工程让模型先在 GLSurfaceView 上动起来再做触摸交互。适合想在自己 App 里用 Live2D、又不想被渲染细节劝退的客户端开发者。2. live2d 在 Android 上的渲染通路为什么不能直接搬桌面版工程2.1 桌面版和移动版的差异OpenGL ES、STL 与文件 IOLive2D 的核心计算库是 C桌面版 demo 通常跑在 OpenGL 3.3 加 GLFW 的窗口环境下而 Android 只提供 OpenGL ES 2.0 / 3.0没有 GLFW也没有桌面 GL 的glGenVertexArrays、glBindVertexArray这类核心函数。很多第一次移植的人把 Windows 工程里的.cpp全部拖进app/src/main/cpp结果第一轮编译就报undefined reference to glfwPollEvents。这不是源码问题而是渲染 API 的差异被忽略了。文件 IO 也是重灾区。桌面版直接std::ifstream读模型路径Android 上没有这个权限模型要么把资源放到assets里通过AssetManager读要么把文件释放到应用私有目录再读还要自己处理路径分隔符。Live2D 的.model3.json里面会引用.moc3、.png、.physics3.json这些相对路径在桌面版解压目录里有效在 Android 上如果解压到私有目录路径要重新拼接。我一般会先确认官方 Native SDK 里的Framework源码自带 Android 分支核心类叫CubismRenderer_OpenGLES2或 ES3 版本。桌面版用的是 OpenGL 渲染器Android 上必须换成 ES 内核否则就算编译过运行时也会直接glGetError报一堆新手错误。内存和标准库同样有坑。NDK 默认的 STL 是libstdc的裁剪版后来切到libc不同 NDK 版本对std::filesystem、std::regex的支持程度不一样。Live2D 的 Framework 代码里会用到std::vector、std::string和部分 C14 特性所以编译选项里最好固定c_static而不是c_shared避免 APK 里少带一个libc_shared.so导致运行时UnsatisfiedLinkError。这一步选错后续所有坑都会叠加出现。2.2 移植前选型官方 Native SDK、预编译 Core 与自研渲染器移植前先要做选型不要一上来就抄桌面 demo。常见做法是使用官方 Native SDK它分三层Core 是模型解析和数学计算的二进制库Framework 是 C 封装负责模型加载、参数更新、物理模拟Sample 只是平台示例。Core 通常以.a静态库提供给不同 ABI它是黑匣子别想着看源码。Framework 是开源的但里面的渲染器要匹配 Core 的版本混用版本会出内存布局错乱。如果你的业务只是做个虚拟形象、互动模型直接用官方 Framework 最快。最后层的渲染器是CubismRenderer_OpenGLES2它已经处理了顶点计算和纹理绑定你只需要在 GL 线程里按顺序调用Update和Draw。自己写渲染器的诱惑在于可以合批、压缩包体但你要自己解析.moc3里的网格、顶点权重和参数索引还要复刻物理效果和表情融合投入成本至少是以周计的。我的经验是除非你有极端的包体积要求或者要做多模型混合渲染的定制引擎否则先走官方框架把 demo 跑通再考虑替换渲染层。选型还要考虑模型资源版本。Live2D 的模型文件有 Cubism 3、4、5 的区别.moc3的二进制格式并不完全兼容。下载模型素材时先看它的 SDK 版本再决定用哪个版本的 Framework。最简单的方法是把模型放到桌面 demo 里跑一次如果能正常呼吸和眨眼再进 Android。跳过这一步后面调 JNI 时会把「模型格式不支持」误判成「CMake 配置错误」。2.3 工具链准备Android Studio、NDK、CMake 与模型资源清单实际的编译环境准备我的建议是直接用一个从 Android Studio 生成的 Native C 工程改省去手写 Gradle 配置的弯路。需要准备四样东西Android SDK、NDK、CMake、一个完整的 Live2D 模型资源目录。NDK 版本和 CMake 版本尽量和 Android Studio 内置的一致避免系统里装了两个 NDK 导致 CMake 找到了错误的工具链。模型资源目录一般由几个文件组成下表是移植前必须核对的最小清单文件作用缺失时后果.model3.json模型入口声明所有资源和参数加载直接失败.moc3网格、顶点、参数数据没有网格可绘制.png贴图白块或黑块.physics3.json物理模拟头发、裙摆等模型能动但无物理效果.motion3.json表情和动作无法播放表情拿到 demo 素材后第一步先在桌面详情里跑通确认模型本身没问题再把它复制到 Android 工程。资源落地位置我一般用两种调试期放到应用私有目录/sdcard/Android/data/包名/files/用adb push推进去正式版我直接打进assets通过AssetManager读取。在 demo 阶段用私有目录最快因为改模型不用重新编 APK改完adb push就能重跑。但要注意 Android 11 以后分区存储限制私有目录路径在不同系统上可能变化不能硬编码。3. 用 CMake JNI 把 live2d 编译进 Android从源码到首帧3.1 建立支持 C 的 Android 工程Gradle 里的关键配置在 Android Studio 里新建项目时选择 Native C 模板会生成一个带externalNativeBuild的最小工程。模板默认生成的CMakeLists.txt只是打印一句 hello接下来我们要把它改成 Live2D 的编译入口。先看build.gradle.kts里的关键配置android { defaultConfig { externalNativeBuild { cmake { arguments -DANDROID_STLc_static, -DANDROID_PLATFORMandroid-21, -DANDROID_ABIarm64-v8a,armeabi-v7a cppFlags -stdc17 -fexceptions -fvisibilityhidden } } } externalNativeBuild { cmake { path file(src/main/cpp/CMakeLists.txt) } } }ANDROID_STLc_static决定让 NDK 使用静态链接的 C 标准库这样运行时不需要把libc_shared.so打包进 APK避免不同模块之间 STL 冲突。ANDROID_PLATFORMandroid-21是 API level 21 以上覆盖绝大多数 Android 设备Live2D Framework 对 API 要求不高这个值不用追高。-fvisibilityhidden会隐藏默认符号减小 .so 体积但后面 JNI 符号需要专门处理我会在避坑章节细讲。CMake 路径指向src/main/cpp/CMakeLists.txt建议把 Live2D 相关源码放在这个目录下和 Java 层隔离。目录结构我习惯这样组织app/src/main/cpp/ ├── CMakeLists.txt ├── l2d/ │ ├── Core/ # Core 头文件和 .a 库 │ ├── Framework/ # CubismFramework 源码 │ ├── LAppAllocator.cpp │ ├── LAppModel.cpp │ ├── LAppView.cpp │ └── native_bridge.cpp # JNI 入口3.2 把 CubismFramework 和 Core 接进 CMakeLists宏定义与 ABI 匹配如果你拿到的是不带 CMakeLists 的 Framework 源码可以直接手动把源码文件加进add_library。我一般不会改官方源码目录结构而是头文件包含路径指向它源码列表则按实际需要的模块拉进来。下面是一个最小 CMakeLists 示例cmake_minimum_required(VERSION 3.22.1) project(live2d_demo) set(L2D_ROOT ${CMAKE_CURRENT_LIST_DIR}/l2d) add_library(live2d SHARED ${L2D_ROOT}/native_bridge.cpp ${L2D_ROOT}/LAppModel.cpp ${L2D_ROOT}/LAppAllocator.cpp ${L2D_ROOT}/LAppView.cpp ${L2D_ROOT}/Framework/src/CubismFramework.cpp ${L2D_ROOT}/Framework/src/Model/CubismUserModel.cpp ${L2D_ROOT}/Framework/src/Rendering/OpenGLES2/CubismRenderer_OpenGLES2.cpp # 按实际需要的源文件继续追加 ) target_include_directories(live2d PRIVATE ${L2D_ROOT}/Framework/src ${L2D_ROOT}/Core/include ) target_compile_definitions(live2d PRIVATE CSM_TARGET_ANDROID CSM_TARGET_GLES GLES_SILENCE_DEPRECATION ) target_link_libraries(live2d ${L2D_ROOT}/Core/lib/arm64-v8a/libLive2DCubismCore.a android GLESv3 EGL log )CSM_TARGET_ANDROID会让 Framework 内部走 Android 分支比如用android_asset相关 API 读文件。CSM_TARGET_GLES是关键的宏它负责把渲染器编译成 OpenGL ES 版本少了它就会链接到桌面 GL 的符号直接导致undefined reference。GLES_SILENCE_DEPRECATION只是压掉 GLES 的弃用警告不加也行。链接libLive2DCubismCore.a时要注意 ABI 路径。上面例子写的是arm64-v8a如果你要支持armeabi-v7a就要改成armv7目录下的.a最稳妥的做法是在 CMake 里用ANDROID_ABI变量动态选择target_link_libraries(live2d ${L2D_ROOT}/Core/lib/${ANDROID_ABI}/libLive2DCubismCore.a android GLESv3 EGL log )这样双 ABI 编译时不会被路径卡死。Framework 源码如果自带CMakeLists.txt也可以直接add_subdirectory然后链接它的 target但要注意它是否默认定义了CSM_TARGET_ANDROID。我碰过几次手写源码列表漏文件导致链接错误的所以更推荐先add_subdirectory试一下不行再用上面的手写方式。3.3 写 JNI bridgeJava 持有 nativeHandleC 负责加载与绘制Java 层不能直接调用 C 对象需要通过 JNI 桥接层。我习惯的做法是Java 层只保存一个long类型的nativeHandle它是指向 C 对象的指针值所有操作都通过这个 handle 完成。这样比全局单例干净多模型场景下不会互相覆盖。先看 Java 侧的关键代码public class L2DNative { static { System.loadLibrary(live2d); } public static native long nativeCreate(); public static native void nativeDestroy(long handle); public static native void nativeUpdate(long handle); public static native void nativeDraw(long handle); public static native void nativeOnTouch(long handle, float nx, float ny); }对应 C 侧的native_bridge.cppextern C JNIEXPORT jlong JNICALL Java_com_example_live2d_L2DNative_nativeCreate(JNIEnv* env, jobject thiz) { LAppView* view new LAppView(); view-Initialize(); return reinterpret_castjlong(view); } extern C JNIEXPORT void JNICALL Java_com_example_live2d_L2DNative_nativeDestroy(JNIEnv* env, jobject thiz, jlong handle) { LAppView* view reinterpret_castLAppView*(handle); if (view ! nullptr) { view-Release(); delete view; } } extern C JNIEXPORT void JNICALL Java_com_example_live2d_L2DNative_nativeUpdate(JNIEnv* env, jobject thiz, jlong handle) { reinterpret_castLAppView*(handle)-Update(); } extern C JNIEXPORT void JNICALL Java_com_example_live2d_L2DNative_nativeDraw(JNIEnv* env, jobject thiz, jlong handle) { reinterpret_castLAppView*(handle)-Draw(); }nativeCreate里 new 一个LAppView并返回指针地址Java 层把它存起来。后续每个 native 方法都把这个 long 传回去C 层用reinterpret_cast还原指针。这里有个细节不要用全局变量保存当前 view因为 GLSurfaceView 横竖屏切换或 Activity 重建时JNI 层可能同时存在多个实例。用 handle 传递可以做到一实例一状态。nativeOnTouch接收的nx和ny是 0 到 1 的归一化比例不是像素坐标这是有意为之。Java 层在触控事件里除以视图宽高避免屏幕像素密度和模型分辨率耦合。具体映射逻辑放在 C 层后面避坑章节会详细讲 y 轴翻转。3.4 GLSurfaceView 上的最小绘制路径清屏、更新、DrawGLSurfaceView 的渲染线程和 Android 主线程是分开的Live2D 的模型更新和绘制必须在 GL 线程里串行执行。Renderer 实现里最关键的是onDrawFrame每一帧只做三件事清屏、更新、绘制。Override public void onDrawFrame(GL10 gl) { L2DNative.nativeUpdate(mNativeHandle); GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); L2DNative.nativeDraw(mNativeHandle); }先Update再Draw顺序不能反。Update内部会推进物理模拟、表情混合和参数插值Draw才会把这些计算结果提交成三角形网格。如果先 Draw 再 Update画面会永远比模型状态晚一帧看起来像触摸没反应。C 侧的Draw里还有一些不能省的 OpenGL 状态void LAppView::Draw() { glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); model-Update(); model-Draw(); }glEnable(GL_BLEND)不开启的话贴图的透明区域会变黑块因为默认把 alpha 通道当成了覆盖写。混合函数选GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA是标准 alpha 混合能正确处理 PNG 的半透明边缘。有些版本会建议 premultiplied alpha 用GL_ONE, GL_ONE_MINUS_SRC_ALPHA但 Live2D 贴图标准做法是非预乘别混用。最后别忘了glClearColor要设成需要的背景色。透明背景 demo 可以设置黑色或白色若想观察 alpha 通道用GLES20.glClearColor(0,0,0,0)并且配合视图背景透明度否则屏幕看起来是黑的容易误判成黑屏 bug。4. live2d Android 移植避坑5 条可复现的诊断记录4.1 链接时报 undefined reference原因是宏定义缺失现象编译到CubismRenderer相关源文件时链接器报undefined reference to glGenVertexArrays或glfwWindowHint之类的一串符号错误。原因CMake 里没有定义CSM_TARGET_GLESFramework 走了 OpenGL 桌面分支生成了对 GLFW、GLEW 的调用而 Android 平台上没有这些库。解决在target_compile_definitions里补上CSM_TARGET_ANDROID和CSM_TARGET_GLES然后执行一次完整的 Clean 和 Build。只加宏不 Clean 偶尔还会残留旧的 CMake 缓存遇到奇怪链接问题就先删build目录再编。如果在 Android Studio 的build窗口里看到错误列表包含CubismFramework.cpp直接用命令行排查更快cd app/src/main/cpp grep -n CSM_TARGET_GLES Framework/src/CubismFramework.cpp这样能确认这个宏在源码里到底控制哪一段逻辑。别去猜直接看代码分支。4.2 模型加载成功但画面黑屏纹理参数与混合状态现象Logcat 里能打出.model3.json加载成功模型尺寸也正确但 GLSurfaceView 上只有背景色模型看不见。原因通常有两个方向贴图纹理虽然创建了但没有设置GL_TEXTURE_MIN_FILTER默认 mipmap 滤波对非 mipmap 纹理返回GL_INVALID_ENUM或者没有开启混合状态透明区域把整个模型盖成了黑块。解决在绑定纹理后显式设置采样参数同时确认 Draw 前调用glEnable(GL_BLEND)glBindTexture(GL_TEXTURE_2D, textureId); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glPixelStorei(GL_UNPACK_ALIGNMENT, 1);GL_UNPACK_ALIGNMENT设成 1 是因为 iOS/Android 上 PNG 解码后的行字节可能不是 4 的倍数不设会产生贴图错位。黑屏排查时还有一个常用手段在onDrawFrame末尾插入GLES20.glGetError()如果返回值不是 0就从错误码推断是纹理问题还是 shader 问题。血泪经验黑屏先跑一次glGetError能省掉一半猜谜时间。4.3 JNI 崩溃UnsatisfiedLinkError 与符号可见性现象System.loadLibrary(live2d)成功但调用nativeCreate()时抛出java.lang.UnsatisfiedLinkError: No implementation found for ...。原因CMake 里开启了-fvisibilityhidden导致JNIEXPORT宏在某些编译器组合下失效JNI 函数符号被隐藏了。另外也可能是包名/类名写错Java_包名_类名_方法名必须和实际路径完全一致。解决给 JNI 函数增加可见性属性最省事的是在函数上显式写出extern C JNIEXPORT jlong JNICALL __attribute__((visibility(default))) Java_com_example_live2d_L2DNative_nativeCreate(JNIEnv* env, jobject thiz) { ... }还有一个更稳妥的方案用JNI_OnLoad主动注册 native 方法不再依赖Java_*命名规则。这样即使符号被隐藏也能注册成功但代码量稍多一点。我建议项目目标函数少时用命名法加 visibility 属性函数多了再切JNI_OnLoad注册表。4.4 触摸命中偏移坐标空间没归一化现象手指点击模型的头部模型却看向侧下方表情触发区域完全对不上。原因Android 触摸事件返回的是像素坐标原点在左上角y 轴向下Live2D 的模型受点击区域用的是视口比例坐标且 y 轴通常向上。直接把event.getX()传进 C必然偏移到右下角或左下角。解决Java 层先归一化再做 y 轴翻转Override public boolean onTouchEvent(MotionEvent event) { float nx event.getX() / getWidth(); float ny event.getY() / getHeight(); L2DNative.nativeOnTouch(mNativeHandle, nx, ny); requestRender(); return true; }C 层把 0~1 比例换成 Live2D 的坐标范围一般会映射成以中心为原点的坐标系void LAppView::SetTouch(float nx, float ny) { csmFloat32 x (nx - 0.5f) * 2.0f; csmFloat32 y -(ny - 0.5f) * 2.0f; // y 轴翻转 model-SetDragging(x, y); }把 y 取负是必须的否则触摸点会上下颠倒。这里如果用的官方 Sample 里的触摸逻辑最好直接看它内部换算公式因为不同版本的 Framework 坐标方向定义可能有差异。4.5 NDK 版本切换后编译噪音STL 与状态缓存不一致现象同样的源码前几天编译正常升级 NDK 后突然报stoi is not a member of std或者链接时出现undefined reference to std::__throw_bad_alloc()。原因Framework 源码里某些文件依赖#include string但没显式包含旧 NDK 的 libc 头文件恰好间接带进来了新 NDK 收紧后暴露出来另一种可能是ANDROID_STL在 Gradle 里写的是c_shared但某个预编译第三方库用的是c_static两个 STL 实现混在同一个进程里。解决统一用c_static并在 Framework 源文件里补头文件#include string然后做一次 Clean。NDK 升级后最好的习惯是删掉项目根目录下的.cxx和build文件夹再重新构建CMake 的缓存经常让旧路径失效。这个问题的玄学之处在于报错行往往不在真正缺头文件的那一行而是在模板展开深处所以看到stoi就直接查Framework/src里所有没包含string的文件一条条补上就好。5. 编译成功只是开始验证清单与进阶接入技巧demo 能编译、能装上手机只是第一步真正可信的验证不是打开 App 看一屏动画而是把下面五项逐条打勾模型加载日志出现、贴图不是白块、嘴巴和眼睛参数能跟随触摸、物理效果在动、连续跑 30 分钟不闪退。我会在onDrawFrame里用Log.d(Live2D, ...)打印每帧的参数坐标跑一次adb logcat -s Live2D就能确认模型是否在持续更新。接入业务时有一个值得养的细节GLSurfaceView 默认是RENDERMODE_CONTINUOUSLY模型静止时也在按帧渲染耗电又费发热。改成setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY)并且只有触摸、表情变化或动画播放时才调用requestRender()发热量会明显下降。另一个习惯是抽一层Live2DController接口把 JNI handle、生命周期绑定和纹理释放封进去这样后续切换多个模型时不会在 Activity 里堆一堆 native 调用。我自己最容易犯的毛病是 demo 一跑起来就急着往业务里塞结果在低端机上出现纹理泄漏和触摸漂移后来才养成先在验证清单上打勾、再动手接业务的习惯。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑