- Updated Cargo.toml to include freetype-rs for font rendering. - Modified message.rs to introduce a new Message variant for subtitles. - Enhanced HttpPlugin to send subtitle messages to the live2d plugin. - Implemented text rendering in the live2d plugin using FreeType for Chinese characters. - Added OpenGL shaders for rendering text overlays. - Created a new text.rs module for handling text rasterization and texture rendering. - Updated renderer.rs to integrate subtitle rendering logic.
265 lines
8.4 KiB
Rust
265 lines
8.4 KiB
Rust
//! Live2DPlugin — Live2D 原生渲染插件
|
||
//!
|
||
//! 接管 Live2D 模式下的设备端渲染,替代 Firefox kiosk 方案。
|
||
//! 监听 ConfigReloaded 消息,当 render_type=Live2d 时启动原生渲染循环。
|
||
|
||
use crate::core::config::RenderType;
|
||
use crate::core::message::{Destination, Envelope, Message};
|
||
use crate::core::plugin::{Platform, Plugin, PluginContext, PluginInfo};
|
||
use anyhow::{Context, Result};
|
||
use std::path::PathBuf;
|
||
use std::sync::atomic::{AtomicBool, Ordering};
|
||
use std::sync::{Arc, Mutex};
|
||
use std::thread::JoinHandle;
|
||
use std::time::{Duration, Instant};
|
||
|
||
use super::renderer::{Live2DRenderer, Subtitle, SubtitleState};
|
||
|
||
pub struct Live2DPlugin {
|
||
ctx: Option<PluginContext>,
|
||
/// 渲染器句柄(运行在独立线程)
|
||
renderer: Option<Arc<std::sync::Mutex<Live2DRenderer>>>,
|
||
worker: Option<JoinHandle<()>>,
|
||
/// 渲染停止信号
|
||
stop_flag: Arc<AtomicBool>,
|
||
/// 字幕共享状态(http 插件写入,渲染线程读取)
|
||
subtitle: SubtitleState,
|
||
}
|
||
|
||
impl Live2DPlugin {
|
||
pub fn new() -> Self {
|
||
Self {
|
||
ctx: None,
|
||
renderer: None,
|
||
worker: None,
|
||
stop_flag: Arc::new(AtomicBool::new(false)),
|
||
subtitle: Arc::new(Mutex::new(None)),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Default for Live2DPlugin {
|
||
fn default() -> Self {
|
||
Self::new()
|
||
}
|
||
}
|
||
|
||
impl Plugin for Live2DPlugin {
|
||
fn id(&self) -> &str {
|
||
"live2d"
|
||
}
|
||
|
||
fn info(&self) -> PluginInfo {
|
||
PluginInfo {
|
||
name: "Live2D Renderer".to_string(),
|
||
version: "0.1.0".to_string(),
|
||
description: "Live2D 原生渲染(Cubism Core FFI + OpenGL ES 2.0)".to_string(),
|
||
platform: Platform::Linux,
|
||
}
|
||
}
|
||
|
||
fn dependencies(&self) -> Vec<String> {
|
||
vec![]
|
||
}
|
||
|
||
fn init(&mut self, ctx: PluginContext) -> Result<()> {
|
||
self.ctx = Some(ctx);
|
||
Ok(())
|
||
}
|
||
|
||
fn start(&mut self) -> Result<()> {
|
||
let ctx = self
|
||
.ctx
|
||
.as_ref()
|
||
.context("live2d plugin context is not initialized")?;
|
||
|
||
ctx.tx.send(Envelope {
|
||
from: self.id().to_string(),
|
||
to: Destination::Manager,
|
||
message: Message::PluginReady(self.id().to_string()),
|
||
})?;
|
||
|
||
// 如果初始配置就是 Live2D 模式,立即启动渲染
|
||
let config = (*ctx.config).clone();
|
||
if config.character.render_type == RenderType::Live2d {
|
||
println!("[Live2DPlugin] 检测到初始配置为 Live2D 模式,启动渲染");
|
||
self.start_live2d(&config)?;
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn handle_message(&mut self, msg: Message) -> Result<()> {
|
||
match msg {
|
||
Message::ConfigReloaded(config) => {
|
||
if config.character.render_type == RenderType::Live2d {
|
||
self.start_live2d(&config)?;
|
||
} else {
|
||
self.stop_live2d();
|
||
}
|
||
}
|
||
Message::Shutdown => {
|
||
self.stop_live2d();
|
||
}
|
||
Message::Subtitle { text } => {
|
||
// 设备端字幕:写入共享状态,渲染线程每帧读取,超时自动消失
|
||
let ttl = Duration::from_millis(
|
||
(text.chars().count().max(1) as u64)
|
||
.saturating_mul(220)
|
||
.saturating_add(2500),
|
||
);
|
||
*self.subtitle.lock().unwrap() =
|
||
Some(Subtitle { text, expires_at: Instant::now() + ttl });
|
||
}
|
||
_ => {}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
fn stop(&mut self) -> Result<()> {
|
||
self.stop_live2d();
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
impl Live2DPlugin {
|
||
fn start_live2d(&mut self, config: &crate::core::config::AppConfig) -> Result<()> {
|
||
// 先停止旧的渲染
|
||
self.stop_live2d();
|
||
|
||
let model_rel = config
|
||
.character
|
||
.live2d_model
|
||
.clone()
|
||
.context("未配置 live2d_model")?;
|
||
|
||
// Core .so 路径(相对项目根目录或绝对路径)
|
||
let core_so = find_core_so()?;
|
||
|
||
// live2d 资源根目录
|
||
let live2d_base = find_live2d_base(config)?;
|
||
|
||
let width = config.display.render_width as i32;
|
||
let height = config.display.render_height as i32;
|
||
|
||
let stop_flag = Arc::clone(&self.stop_flag);
|
||
stop_flag.store(false, Ordering::SeqCst);
|
||
|
||
let tx = self
|
||
.ctx
|
||
.as_ref()
|
||
.context("no ctx")?
|
||
.tx
|
||
.clone();
|
||
|
||
let renderer = Arc::new(std::sync::Mutex::new(
|
||
Live2DRenderer::new(
|
||
&core_so,
|
||
&live2d_base,
|
||
&model_rel,
|
||
width,
|
||
height,
|
||
Arc::clone(&self.subtitle),
|
||
)?,
|
||
));
|
||
self.renderer = Some(Arc::clone(&renderer));
|
||
|
||
// 渲染器初始化(含 texture 上传)已完成,释放主线程的 EGL 上下文绑定
|
||
// 让渲染子线程能重新绑定(同一个 EGL context 不能同时在两个线程 current)
|
||
{
|
||
let r = renderer.lock().unwrap();
|
||
r.gl_ctx.release_current();
|
||
}
|
||
|
||
let handle = std::thread::spawn(move || {
|
||
// EGL 上下文是线程绑定的,需在渲染线程重新绑定
|
||
{
|
||
let r = renderer.lock().unwrap();
|
||
if let Err(e) = r.gl_ctx.make_current() {
|
||
eprintln!("[Live2DPlugin] eglMakeCurrent 失败: {e}");
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 渲染循环
|
||
loop {
|
||
if stop_flag.load(Ordering::SeqCst) {
|
||
break;
|
||
}
|
||
// 检查 talking 状态(从 HTTP 插件共享的状态读取,这里通过轮询 config 实现)
|
||
// 简化:直接渲染
|
||
let render_result = {
|
||
let mut r = renderer.lock().unwrap();
|
||
r.render_frame()
|
||
};
|
||
if let Err(e) = render_result {
|
||
eprintln!("[Live2DPlugin] 渲染失败: {e}");
|
||
let _ = tx.send(Envelope {
|
||
from: "live2d".to_string(),
|
||
to: Destination::Manager,
|
||
message: Message::StateChanged {
|
||
old_state: "live2d_running".into(),
|
||
new_state: "live2d_error".into(),
|
||
},
|
||
});
|
||
break;
|
||
}
|
||
std::thread::sleep(std::time::Duration::from_millis(16));
|
||
}
|
||
});
|
||
self.worker = Some(handle);
|
||
|
||
println!("[Live2DPlugin] Live2D 原生渲染已启动: {model_rel}");
|
||
Ok(())
|
||
}
|
||
|
||
fn stop_live2d(&mut self) {
|
||
self.stop_flag.store(true, Ordering::SeqCst);
|
||
if let Some(handle) = self.worker.take() {
|
||
let _ = handle.join();
|
||
}
|
||
self.renderer = None;
|
||
}
|
||
}
|
||
|
||
/// 查找 libLive2DCubismCore.so
|
||
fn find_core_so() -> Result<String> {
|
||
// 候选路径优先级:
|
||
// 1. 环境变量 SHOWEN_LIVE2D_CORE_SO
|
||
// 2. 项目根 extern/libLive2DCubismCore.so(运行时随二进制一起部署)
|
||
// 3. /usr/lib/libLive2DCubismCore.so
|
||
// 4. /home/showen/Showen/ShowenV2/extern/libLive2DCubismCore.so
|
||
if let Ok(p) = std::env::var("SHOWEN_LIVE2D_CORE_SO") {
|
||
if std::path::Path::new(&p).exists() {
|
||
return Ok(p);
|
||
}
|
||
}
|
||
let candidates = [
|
||
"extern/libLive2DCubismCore.so",
|
||
"/usr/lib/libLive2DCubismCore.so",
|
||
"/usr/local/lib/libLive2DCubismCore.so",
|
||
"/home/showen/Showen/ShowenV2/extern/libLive2DCubismCore.so",
|
||
];
|
||
for c in &candidates {
|
||
if std::path::Path::new(c).exists() {
|
||
return Ok(c.to_string());
|
||
}
|
||
}
|
||
anyhow::bail!("找不到 libLive2DCubismCore.so,请设置环境变量 SHOWEN_LIVE2D_CORE_SO 或部署到 extern/")
|
||
}
|
||
|
||
/// 查找 live2d 资源根目录
|
||
fn find_live2d_base(config: &crate::core::config::AppConfig) -> Result<PathBuf> {
|
||
let candidates = [
|
||
PathBuf::from("configs/live2d"),
|
||
PathBuf::from("/home/showen/Showen/ShowenV2/configs/live2d"),
|
||
std::env::current_dir()?.join("configs/live2d"),
|
||
];
|
||
for c in &candidates {
|
||
if c.exists() {
|
||
return Ok(c.clone());
|
||
}
|
||
}
|
||
anyhow::bail!("找不到 configs/live2d 目录")
|
||
}
|