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:
239
src/plugins/live2d/text.rs
Normal file
239
src/plugins/live2d/text.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user