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

View File

@@ -205,6 +205,29 @@ void main() {
}
"#;
/// 文字 overlay 着色器(像素坐标 -> 裁剪空间,采样字形纹理,按 u_color 着色)
pub const TEXT_VERT_SHADER_SRC: &str = r#"#version 100
attribute vec2 a_position;
attribute vec2 a_texCoord;
uniform mat4 u_matrix;
varying vec2 v_uv;
void main() {
gl_Position = u_matrix * vec4(a_position, 0.0, 1.0);
v_uv = a_texCoord;
}
"#;
pub const TEXT_FRAG_SHADER_SRC: &str = r#"#version 100
precision mediump float;
varying vec2 v_uv;
uniform sampler2D s_texture0;
uniform vec4 u_color;
void main() {
float a = texture2D(s_texture0, v_uv).a;
gl_FragColor = vec4(u_color.rgb, a * u_color.a);
}
"#;
/// 遮罩mask着色器仅把 mask drawable 的不透明区域写入 stencil
/// 用于裁切被遮罩的 drawable如夹在身体里的后臂、被头发遮挡的脸
pub const MASK_VERT_SHADER_SRC: &str = r#"#version 100
@@ -255,6 +278,12 @@ pub struct Gl {
config_size: egl::EGLint,
num_config: *mut egl::EGLint,
) -> egl::EGLBoolean,
pub egl_get_config_attrib: unsafe extern "C" fn(
dpy: egl::EGLDisplay,
config: egl::EGLConfig,
attribute: egl::EGLint,
value: *mut egl::EGLint,
) -> egl::EGLBoolean,
pub egl_create_window_surface: unsafe extern "C" fn(
dpy: egl::EGLDisplay,
config: egl::EGLConfig,
@@ -433,6 +462,7 @@ impl Gl {
egl_get_display: *lib_egl.get(b"eglGetDisplay\0")?,
egl_initialize: *lib_egl.get(b"eglInitialize\0")?,
egl_choose_config: *lib_egl.get(b"eglChooseConfig\0")?,
egl_get_config_attrib: *lib_egl.get(b"eglGetConfigAttrib\0")?,
egl_create_window_surface: *lib_egl.get(b"eglCreateWindowSurface\0")?,
egl_create_context: *lib_egl.get(b"eglCreateContext\0")?,
egl_make_current: *lib_egl.get(b"eglMakeCurrent\0")?,
@@ -530,6 +560,13 @@ pub struct RenderContext {
pub mask_uniform_texture: GLint,
pub mask_uniform_multiply_color: GLint,
pub mask_uniform_screen_color: GLint,
// 文字 overlay 程序
pub text_program: GLuint,
pub text_uniform_matrix: GLint,
pub text_uniform_color: GLint,
pub text_uniform_texture: GLint,
pub text_attrib_position: GLint,
pub text_attrib_tex_coord: GLint,
pub width: i32,
pub height: i32,
}
@@ -714,6 +751,18 @@ impl RenderContext {
num_config
);
// 真实查询 chosen config 的 depth/stencil 位数,确认 stencil 缓冲真正分配。
let mut stencil_size: egl::EGLint = -1;
let mut depth_size: egl::EGLint = -1;
unsafe {
(gl.egl_get_config_attrib)(display, config, egl::EGL_STENCIL_SIZE, &mut stencil_size);
(gl.egl_get_config_attrib)(display, config, egl::EGL_DEPTH_SIZE, &mut depth_size);
}
println!(
"[Live2D] EGL 实际分配: depth={} stencil={} (stencil<=0 表示遮罩不可用)",
depth_size, stencil_size
);
let surface = unsafe {
(gl.egl_create_window_surface)(display, config, x_window as egl::NativeWindowType, std::ptr::null())
};
@@ -846,6 +895,44 @@ impl RenderContext {
unsafe { (gl.gl_get_uniform_location)(mask_program, c.as_ptr()) }
};
// 文字 overlay 程序
let text_vert = compile_shader(&gl, gl::GL_VERTEX_SHADER, TEXT_VERT_SHADER_SRC)?;
let text_frag = compile_shader(&gl, gl::GL_FRAGMENT_SHADER, TEXT_FRAG_SHADER_SRC)?;
let text_program = unsafe {
let p = (gl.gl_create_program)();
(gl.gl_attach_shader)(p, text_vert);
(gl.gl_attach_shader)(p, text_frag);
(gl.gl_link_program)(p);
let mut status = 0;
(gl.gl_get_programiv)(p, gl::GL_LINK_STATUS, &mut status);
if status == 0 {
let mut log: Vec<u8> = vec![0u8; 1024];
(gl.gl_get_program_info_log)(p, 1024, std::ptr::null_mut(), log.as_mut_ptr() as *mut c_char);
return Err(anyhow!("text shader link failed: {}", String::from_utf8_lossy(&log)));
}
p
};
let text_uniform_matrix = {
let c = std::ffi::CString::new("u_matrix")?;
unsafe { (gl.gl_get_uniform_location)(text_program, c.as_ptr()) }
};
let text_uniform_color = {
let c = std::ffi::CString::new("u_color")?;
unsafe { (gl.gl_get_uniform_location)(text_program, c.as_ptr()) }
};
let text_uniform_texture = {
let c = std::ffi::CString::new("s_texture0")?;
unsafe { (gl.gl_get_uniform_location)(text_program, c.as_ptr()) }
};
let text_attrib_position = {
let c = std::ffi::CString::new("a_position")?;
unsafe { (gl.gl_get_attrib_location)(text_program, c.as_ptr()) }
};
let text_attrib_tex_coord = {
let c = std::ffi::CString::new("a_texCoord")?;
unsafe { (gl.gl_get_attrib_location)(text_program, c.as_ptr()) }
};
println!(
"[Live2D] GL 程序就绪: program={} pos={} tex={} mat={} tex_u={} base={} mult={} scr={}",
program, attrib_position, attrib_tex_coord, uniform_matrix,
@@ -882,6 +969,12 @@ impl RenderContext {
mask_uniform_texture,
mask_uniform_multiply_color,
mask_uniform_screen_color,
text_program,
text_uniform_matrix,
text_uniform_color,
text_uniform_texture,
text_attrib_position,
text_attrib_tex_coord,
width: win_w,
height: win_h,
})