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.
This commit is contained in:
2026-07-09 09:15:23 +08:00
parent 9e31aee42d
commit 7cb8cee70d
10 changed files with 717 additions and 14 deletions

239
src/plugins/live2d/text.rs Normal file
View File

@@ -0,0 +1,239 @@
//! 基于 FreeType 的中文文字光栅化 + OpenGL 纹理渲染(设备端 Live2D 字幕 overlay
//!
//! 设计:每个字符首次出现时用 FreeType 渲染为灰度位图(经 `render_glyph` 拿到位图),转成
//! 白色字形alpha=灰度)的 RGBA 纹理并缓存;绘制时按 pen 位置贴纹理即可。纹理创建需要
//! `Gl` 函数指针(与渲染器共用同一 EGL 上下文)。
use super::gl::gl as gl_const;
use super::gl::Gl;
use freetype::face::LoadFlag;
use freetype::Face;
use freetype::Library;
use freetype::RenderMode;
use std::collections::HashMap;
use std::path::Path;
use anyhow::Result;
/// 单个字符的缓存字形
#[derive(Clone)]
pub struct CachedGlyph {
/// 字形纹理0 表示该字符无可见像素,仅用于步进)
pub tex: gl_const::GLuint,
pub w: i32,
pub h: i32,
/// 字形位图相对 pen 原点的偏移
pub left: i32,
pub top: i32,
/// 26.6 定点像素步进(渲染端需 >> 6 转为像素)
pub advance: i32,
}
pub struct TextRenderer {
face: Face<'static>,
font_px: u32,
glyphs: HashMap<char, CachedGlyph>,
white_tex: Option<gl_const::GLuint>,
}
// FreeType 的 Face 包裹裸指针,仅在本插件单一渲染线程使用。
unsafe impl Send for TextRenderer {}
/// FT_LOAD_RENDER 标志位0x4加载字符的同时渲染为位图
const FT_LOAD_RENDER: i32 = 0x4;
impl TextRenderer {
pub fn new(font_path: &Path, font_px: u32) -> Result<Self> {
let library = Library::init()?;
// 让 Library 成为 'static程序级单例从而 Face<'static> 可长期持有而不会悬垂
let library: &'static Library = Box::leak(Box::new(library));
let face = library.new_face(font_path, 0)?;
let tr = Self {
face,
font_px,
glyphs: HashMap::new(),
white_tex: None,
};
tr.face.set_pixel_sizes(0, font_px)?;
println!("[Live2D] 字幕字体就绪: {:?} ({}px)", font_path, font_px);
Ok(tr)
}
/// 当前字号(像素)
pub fn font_size(&self) -> u32 {
self.font_px
}
/// 1x1 白色纹理用于纯色背景框alpha 恒为 1
fn white_tex(&mut self, gl: &Gl) -> gl_const::GLuint {
if let Some(t) = self.white_tex {
return t;
}
let mut tex = 0u32;
unsafe {
(gl.gl_gen_textures)(1, &mut tex);
(gl.gl_bind_texture)(gl_const::GL_TEXTURE_2D, tex);
(gl.gl_tex_parameteri)(
gl_const::GL_TEXTURE_2D,
gl_const::GL_TEXTURE_MIN_FILTER,
gl_const::GL_LINEAR as i32,
);
(gl.gl_tex_parameteri)(
gl_const::GL_TEXTURE_2D,
gl_const::GL_TEXTURE_MAG_FILTER,
gl_const::GL_LINEAR as i32,
);
(gl.gl_tex_parameteri)(
gl_const::GL_TEXTURE_2D,
gl_const::GL_TEXTURE_WRAP_S,
gl_const::GL_CLAMP_TO_EDGE as i32,
);
(gl.gl_tex_parameteri)(
gl_const::GL_TEXTURE_2D,
gl_const::GL_TEXTURE_WRAP_T,
gl_const::GL_CLAMP_TO_EDGE as i32,
);
let px: [u8; 4] = [255, 255, 255, 255];
(gl.gl_tex_image_2d)(
gl_const::GL_TEXTURE_2D,
0,
gl_const::GL_RGBA as i32,
1,
1,
0,
gl_const::GL_RGBA,
gl_const::GL_UNSIGNED_BYTE,
px.as_ptr() as *const std::ffi::c_void,
);
}
self.white_tex = Some(tex);
tex
}
/// 取 1x1 白纹理(供外部画背景框)。
pub fn white_texture(&mut self, gl: &Gl) -> gl_const::GLuint {
self.white_tex(gl)
}
/// 估算字符像素宽度(用于换行布局,不创建纹理)。返回像素值。
pub fn measure_advance(&mut self, ch: char) -> i32 {
if self
.face
.load_char(ch as usize, LoadFlag::from_bits_truncate(FT_LOAD_RENDER))
.is_ok()
{
(self.face.glyph().advance().x >> 6) as i32
} else {
(self.font_px as f32 * 0.5) as i32
}
}
fn rasterize(&mut self, gl: &Gl, ch: char) -> Option<CachedGlyph> {
let half_advance_26_6 = (self.font_px as i32) << 5; // 半字宽26.6 定点
if self
.face
.load_char(ch as usize, LoadFlag::from_bits_truncate(FT_LOAD_RENDER))
.is_err()
{
return Some(CachedGlyph {
tex: 0,
w: 0,
h: 0,
left: 0,
top: 0,
advance: half_advance_26_6,
});
}
let glyph = self.face.glyph();
if glyph.render_glyph(RenderMode::Normal).is_err() {
let adv = glyph.advance().x as i32; // 26.6 定点
return Some(CachedGlyph {
tex: 0,
w: 0,
h: 0,
left: 0,
top: 0,
advance: adv,
});
}
let bitmap = glyph.bitmap();
let w = bitmap.width();
let h = bitmap.rows();
let left = glyph.bitmap_left();
let top = glyph.bitmap_top();
let advance = glyph.advance().x as i32; // 26.6 定点
if w == 0 || h == 0 {
return Some(CachedGlyph {
tex: 0,
w: 0,
h: 0,
left,
top,
advance,
});
}
// 灰度 -> RGBA白色字形alpha=灰度
let buf = bitmap.buffer();
let mut rgba: Vec<u8> = Vec::with_capacity((w * h * 4) as usize);
for &g in buf {
rgba.push(255);
rgba.push(255);
rgba.push(255);
rgba.push(g);
}
let mut tex = 0u32;
unsafe {
(gl.gl_gen_textures)(1, &mut tex);
(gl.gl_bind_texture)(gl_const::GL_TEXTURE_2D, tex);
(gl.gl_tex_parameteri)(
gl_const::GL_TEXTURE_2D,
gl_const::GL_TEXTURE_MIN_FILTER,
gl_const::GL_LINEAR as i32,
);
(gl.gl_tex_parameteri)(
gl_const::GL_TEXTURE_2D,
gl_const::GL_TEXTURE_MAG_FILTER,
gl_const::GL_LINEAR as i32,
);
(gl.gl_tex_parameteri)(
gl_const::GL_TEXTURE_2D,
gl_const::GL_TEXTURE_WRAP_S,
gl_const::GL_CLAMP_TO_EDGE as i32,
);
(gl.gl_tex_parameteri)(
gl_const::GL_TEXTURE_2D,
gl_const::GL_TEXTURE_WRAP_T,
gl_const::GL_CLAMP_TO_EDGE as i32,
);
(gl.gl_tex_image_2d)(
gl_const::GL_TEXTURE_2D,
0,
gl_const::GL_RGBA as i32,
w,
h,
0,
gl_const::GL_RGBA,
gl_const::GL_UNSIGNED_BYTE,
rgba.as_ptr() as *const std::ffi::c_void,
);
}
Some(CachedGlyph {
tex,
w,
h,
left,
top,
advance,
})
}
/// 获取(必要时光栅化并缓存)字符字形。
pub fn glyph(&mut self, gl: &Gl, ch: char) -> Option<CachedGlyph> {
if !self.glyphs.contains_key(&ch) {
if let Some(g) = self.rasterize(gl, ch) {
self.glyphs.insert(ch, g);
}
}
self.glyphs.get(&ch).cloned()
}
}