Files
ShowenV2/src/plugins/live2d/plugin.rs
XiuchengWu 7cb8cee70d feat: add device-side subtitle overlay using FreeType for rendering Chinese text
- 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.
2026-07-09 09:15:23 +08:00

265 lines
8.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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 目录")
}