- 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.
240 lines
7.6 KiB
Rust
240 lines
7.6 KiB
Rust
//! 基于 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()
|
||
}
|
||
}
|