feat(live2d): implement OpenGL ES 2.0 rendering backend with EGL support

- Add `gl.rs` for OpenGL ES 2.0 and EGL context management.
- Introduce `mod.rs` to organize Live2D plugin modules.
- Create `model.rs` for loading and managing Cubism models, including parameter handling and drawable data extraction.
- Implement shader compilation and rendering context setup for Live2D models.
This commit is contained in:
2026-07-07 16:04:49 +08:00
parent 9637bdd323
commit 2aac62b2bb
14 changed files with 1865 additions and 0 deletions

View File

@@ -0,0 +1,155 @@
//! 资源管理model3.json 解析 + PNG 纹理加载
use anyhow::{anyhow, Context, Result};
use serde::Deserialize;
use std::path::{Path, PathBuf};
/// model3.json 中的 Groups 条目
#[derive(Deserialize, Clone, Debug)]
pub struct ModelGroup {
#[serde(rename = "Target")]
pub target: String,
#[serde(rename = "Name")]
pub name: String,
#[serde(rename = "Ids")]
pub ids: Vec<String>,
}
/// model3.json 文件引用
#[derive(Deserialize, Clone, Debug)]
pub struct FileReferences {
#[serde(rename = "Moc")]
pub moc: String,
#[serde(rename = "Textures", default)]
pub textures: Vec<String>,
#[serde(rename = "Physics", default)]
pub physics: Option<String>,
#[serde(rename = "Pose", default)]
pub pose: Option<String>,
#[serde(rename = "DisplayInfo", default)]
pub display_info: Option<String>,
}
/// model3.json 根结构
#[derive(Deserialize, Clone, Debug)]
pub struct Model3Json {
#[serde(rename = "Version")]
pub version: i32,
#[serde(rename = "FileReferences")]
pub file_references: FileReferences,
#[serde(rename = "Groups", default)]
pub groups: Vec<ModelGroup>,
}
impl Model3Json {
/// 从文件解析
pub fn from_path(path: &Path) -> Result<Self> {
let text = std::fs::read_to_string(path)
.with_context(|| format!("读取 model3.json 失败: {}", path.display()))?;
let json: Model3Json = serde_json::from_str(&text)
.with_context(|| format!("解析 model3.json 失败: {}", path.display()))?;
Ok(json)
}
/// 获取 LipSync 参数 ID 列表
pub fn lip_sync_ids(&self) -> Vec<String> {
self.groups
.iter()
.find(|g| g.target == "Parameter" && g.name == "LipSync")
.map(|g| g.ids.clone())
.unwrap_or_else(|| vec!["ParamMouthOpenY".to_string()])
}
/// 获取 EyeBlink 参数 ID 列表
pub fn eye_blink_ids(&self) -> Vec<String> {
self.groups
.iter()
.find(|g| g.target == "Parameter" && g.name == "EyeBlink")
.map(|g| g.ids.clone())
.unwrap_or_else(|| {
vec!["ParamEyeLOpen".to_string(), "ParamEyeROpen".to_string()]
})
}
}
/// 加载的纹理图像RGBA
pub struct TextureImage {
pub width: u32,
pub height: u32,
pub rgba: Vec<u8>,
}
/// 加载 PNG 纹理(使用 image crate
pub fn load_texture(path: &Path) -> Result<TextureImage> {
let img = image::open(path)
.with_context(|| format!("加载纹理失败: {}", path.display()))?
.to_rgba8();
let width = img.width();
let height = img.height();
let rgba = img.into_raw();
Ok(TextureImage {
width,
height,
rgba,
})
}
/// 加载 model3.json 引用的所有纹理(返回按 texture_00, texture_01 顺序的列表)
pub fn load_all_textures(
model3_dir: &Path,
file_refs: &FileReferences,
) -> Result<Vec<TextureImage>> {
let mut textures = Vec::new();
for tex_rel in &file_refs.textures {
let tex_path = model3_dir.join(tex_rel);
let img = load_texture(&tex_path)?;
println!(
"[Live2D] 纹理加载: {} ({}x{})",
tex_rel, img.width, img.height
);
textures.push(img);
}
Ok(textures)
}
/// 解析 model3.json 并返回完整路径集
pub struct ModelAssetPaths {
pub model3_json: PathBuf,
pub model3_dir: PathBuf,
pub moc3: PathBuf,
pub textures: Vec<PathBuf>,
}
/// 从 model3.json 相对路径(如 "haru/Haru.model3.json")解析资源路径
pub fn resolve_asset_paths(
live2d_base_dir: &Path,
model3_json_rel: &str,
) -> Result<ModelAssetPaths> {
let model3_json = live2d_base_dir.join(model3_json_rel);
if !model3_json.exists() {
return Err(anyhow!(
"model3.json 不存在: {}",
model3_json.display()
));
}
let model3_dir = model3_json
.parent()
.ok_or_else(|| anyhow!("无法获取 model3.json 的父目录"))?
.to_path_buf();
let model3 = Model3Json::from_path(&model3_json)?;
let moc3 = model3_dir.join(&model3.file_references.moc);
let textures = model3
.file_references
.textures
.iter()
.map(|t| model3_dir.join(t))
.collect();
Ok(ModelAssetPaths {
model3_json,
model3_dir,
moc3,
textures,
})
}