feat: 添加 Live2D 渲染插件及相关依赖,优化构建和启动脚本

This commit is contained in:
2026-07-08 17:32:56 +08:00
parent a8d7c03139
commit 547085c819
15 changed files with 306 additions and 70 deletions

View File

@@ -1,6 +1,7 @@
#!/bin/bash
cd /home/showen/Showen/ShowenV2
export PATH=/home/showen/.rustup/toolchains/stable-aarch64-unknown-linux-gnu/bin:$PATH
cargo check 2>&1 | grep -E "^error" | head -60
echo "---SUMMARY---"
cargo check 2>&1 | tail -3
pkill -9 -f showen_v2 2>/dev/null
sleep 1
cargo build --release 2>&1 | tail -3
echo "BUILD_EXIT=$?"

11
.run.sh Normal file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
killall showen_v2 2>/dev/null
sleep 1
cd /home/showen/Showen/ShowenV2
DISPLAY=:0 RUST_LOG=info nohup ./target/release/showen_v2 --config configs/live2d_anime.json > /tmp/sl2.log 2>&1 &
echo "PID=$!"
sleep 8
echo "=== LOG ==="
cat /tmp/sl2.log | tail -50
echo "=== PS ==="
ps aux | grep showen_v2 | grep -v grep

12
.start.sh Normal file
View File

@@ -0,0 +1,12 @@
#!/bin/bash
# 杀旧进程
pkill -9 -f showen_v2 2>/dev/null
sleep 1
cd /home/showen/Showen/ShowenV2
export DISPLAY=:0
export RUST_LOG=info
# 后台启动
nohup ./target/release/showen_v2 --config configs/live2d_anime.json > /tmp/sl2.log 2>&1 &
echo "PID=$!"

64
Cargo.lock generated
View File

@@ -65,12 +65,24 @@ dependencies = [
"objc2",
]
[[package]]
name = "bytemuck"
version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "1.11.1"
@@ -251,6 +263,15 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "fdeflate"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
dependencies = [
"simd-adler32",
]
[[package]]
name = "filetime"
version = "0.2.27"
@@ -604,6 +625,19 @@ dependencies = [
"icu_properties",
]
[[package]]
name = "image"
version = "0.25.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
dependencies = [
"bytemuck",
"byteorder-lite",
"moxcms",
"num-traits",
"png",
]
[[package]]
name = "indexmap"
version = "2.13.0"
@@ -734,6 +768,16 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "moxcms"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
dependencies = [
"num-traits",
"pxfm",
]
[[package]]
name = "multer"
version = "2.1.0"
@@ -874,6 +918,19 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]]
name = "png"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
dependencies = [
"bitflags",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
]
[[package]]
name = "potential_utf"
version = "0.1.4"
@@ -901,6 +958,12 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "pxfm"
version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
[[package]]
name = "quote"
version = "1.0.45"
@@ -1164,6 +1227,7 @@ dependencies = [
"dbus-crossroads",
"flate2",
"futures-util",
"image",
"libloading",
"opencv",
"rand",

BIN
s3.xwd Normal file

Binary file not shown.

BIN
s4.xwd Normal file

Binary file not shown.

BIN
s6.xwd Normal file

Binary file not shown.

View File

@@ -3,7 +3,7 @@
//! 直接对应 `Live2DCubismCore.h` 的 C API。
//! 动态加载 libLive2DCubismCore.so需 libm.so 预加载,因 Core 内部使用 powf
use libloading::{Library, Symbol};
use libloading::Library;
use std::os::raw::{c_char, c_int, c_uchar, c_uint, c_void};
/// moc 对齐要求(字节)
@@ -32,6 +32,7 @@ pub mod dynamic_flags {
/// 混合模式
pub mod blend_mode {
use std::os::raw::c_int;
pub const NORMAL: c_int = 0;
pub const ADDITIVE: c_int = 1;
pub const MULTIPLICATIVE: c_int = 2;

View File

@@ -40,7 +40,7 @@ pub mod egl {
// ============ X11 ============
pub mod x11 {
use std::os::raw::{c_int, c_long, c_uint, c_void};
use std::os::raw::{c_int, c_long, c_void};
pub type Display = c_void;
pub type Window = c_long;
@@ -68,6 +68,27 @@ pub mod x11 {
pub const CWEventMask: c_long = 1 << 11;
pub const ExposureMask: c_long = 1 << 15;
pub const StructureNotifyMask: c_long = 1 << 17;
pub const SubstructureNotifyMask: c_long = 1 << 19;
pub const SubstructureRedirectMask: c_long = 1 << 20;
/// XEvent union简化为最大 192 字节的字节数组)
#[repr(C)]
pub struct XEvent {
pub data: [std::os::raw::c_char; 192],
}
/// XClientMessageEvent 布局(用于 _NET_WM_STATE 客户端消息)
#[repr(C)]
pub struct XClientMessageEvent {
pub ftype: c_long, // type (ClientMessage = 33)
pub serial: c_long, // serial
pub send_event: c_int, // send_event
pub display: *mut c_void, // display
pub window: c_long, // window
pub message_type: c_long, // message_type atom
pub format: c_int, // format (32)
pub data_l: [c_long; 5], // data.l
}
}
// ============ GLES2 ============
@@ -291,6 +312,33 @@ pub struct Gl {
pub x_store_name:
unsafe extern "C" fn(disp: *mut x11::Display, w: x11::Window, name: *const c_char) -> c_int,
pub x_flush: unsafe extern "C" fn(disp: *mut x11::Display) -> c_int,
pub x_default_root_window: unsafe extern "C" fn(disp: *mut x11::Display) -> x11::Window,
pub x_sync: unsafe extern "C" fn(disp: *mut x11::Display, discard: c_int) -> c_int,
pub x_raise_window: unsafe extern "C" fn(disp: *mut x11::Display, w: x11::Window) -> c_int,
pub x_display_width: unsafe extern "C" fn(disp: *mut x11::Display, screen: c_int) -> c_int,
pub x_display_height: unsafe extern "C" fn(disp: *mut x11::Display, screen: c_int) -> c_int,
pub x_intern_atom: unsafe extern "C" fn(
disp: *mut x11::Display,
atom_name: *const c_char,
only_if_exists: c_int,
) -> c_long,
pub x_change_property: unsafe extern "C" fn(
disp: *mut x11::Display,
w: x11::Window,
property: c_long,
ftype: c_long,
format: c_int,
mode: c_int,
data: *const c_void,
nelements: c_int,
) -> c_int,
pub x_send_event: unsafe extern "C" fn(
disp: *mut x11::Display,
w: x11::Window,
propagate: c_int,
event_mask: c_long,
event_send: *mut x11::XEvent,
) -> c_int,
}
impl Gl {
@@ -349,6 +397,14 @@ impl Gl {
x_map_window: *lib_x11.get(b"XMapWindow\0")?,
x_store_name: *lib_x11.get(b"XStoreName\0")?,
x_flush: *lib_x11.get(b"XFlush\0")?,
x_default_root_window: *lib_x11.get(b"XDefaultRootWindow\0")?,
x_sync: *lib_x11.get(b"XSync\0")?,
x_raise_window: *lib_x11.get(b"XRaiseWindow\0")?,
x_display_width: *lib_x11.get(b"XDisplayWidth\0")?,
x_display_height: *lib_x11.get(b"XDisplayHeight\0")?,
x_intern_atom: *lib_x11.get(b"XInternAtom\0")?,
x_change_property: *lib_x11.get(b"XChangeProperty\0")?,
x_send_event: *lib_x11.get(b"XSendEvent\0")?,
_lib_egl: lib_egl,
_lib_gl: lib_gl,
@@ -364,6 +420,7 @@ pub struct RenderContext {
pub gl: Gl,
pub display: egl::EGLDisplay,
pub surface: egl::EGLSurface,
pub context: egl::EGLContext,
pub x_display: *mut x11::Display,
pub x_window: x11::Window,
pub program: GLuint,
@@ -395,13 +452,10 @@ fn compile_shader(gl: &Gl, shader_type: GLenum, src: &str) -> Result<GLuint> {
let mut status = 0;
(gl.gl_get_shaderiv)(shader, gl::GL_COMPILE_STATUS, &mut status);
if status == 0 {
let mut log = vec![0i8; 1024];
(gl.gl_get_shader_info_log)(shader, 1024, std::ptr::null_mut(), log.as_mut_ptr());
let log_str = String::from_utf8_lossy(std::slice::from_raw_parts(
log.as_ptr() as *const u8,
1024,
));
return Err(anyhow!("着色器编译失败: {}", log_str));
let mut log: Vec<u8> = vec![0u8; 1024];
(gl.gl_get_shader_info_log)(shader, 1024, std::ptr::null_mut(), log.as_mut_ptr() as *mut c_char);
let log_str = String::from_utf8_lossy(&log);
return Err(anyhow!("shader compile failed: {}", log_str));
}
Ok(shader)
}
@@ -414,8 +468,20 @@ impl RenderContext {
// X11 窗口
let x_display = unsafe { (gl.x_open_display)(std::ptr::null()) };
if x_display.is_null() {
return Err(anyhow!("XOpenDisplay 失败"));
return Err(anyhow!("XOpenDisplay failed"));
}
let root_window = unsafe { (gl.x_default_root_window)(x_display) };
// 使用屏幕实际分辨率,确保全屏覆盖
let screen = 0; // XDefaultScreen(display),通常 0
let screen_w = unsafe { (gl.x_display_width)(x_display, screen) };
let screen_h = unsafe { (gl.x_display_height)(x_display, screen) };
// 用传入的 width/height 创建窗口(配置值),不用屏幕分辨率
// EGL surface 尺寸需与窗口匹配,大窗口可能导致 GPU 限制问题)
let win_w = width;
let win_h = height;
println!("[Live2D] 窗口尺寸: {}x{} (屏幕 {}x{})", win_w, win_h, screen_w, screen_h);
let mut attrs = x11::XSetWindowAttributes {
background_pixmap: 0,
background_pixel: 0,
@@ -429,34 +495,66 @@ impl RenderContext {
save_under: 0,
event_mask: x11::ExposureMask | x11::StructureNotifyMask,
do_not_propagate_mask: 0,
override_redirect: 1,
override_redirect: 0,
colormap: 0,
cursor: 0,
};
let x_window = unsafe {
(gl.x_create_window)(
x_display,
root_window,
0,
0,
0,
width as c_uint,
height as c_uint,
win_w as c_uint,
win_h as c_uint,
0,
24,
1,
std::ptr::null_mut(),
x11::CWOverrideRedirect | x11::CWEventMask,
x11::CWEventMask,
&mut attrs,
)
};
if x_window == 0 {
return Err(anyhow!("XCreateWindow 失败"));
return Err(anyhow!("XCreateWindow failed"));
}
unsafe {
let title = std::ffi::CString::new("ShowenV2 Live2D")?;
(gl.x_store_name)(x_display, x_window, title.as_ptr());
// 让 kwin 把窗口设为全屏_NET_WM_STATE_FULLSCREEN
// 通过发送 ClientMessage 事件给 root window
let net_wm_state = (gl.x_intern_atom)(x_display, b"_NET_WM_STATE\0".as_ptr() as *const c_char, 1);
let net_wm_state_fs = (gl.x_intern_atom)(x_display, b"_NET_WM_STATE_FULLSCREEN\0".as_ptr() as *const c_char, 1);
let net_wm_state_above = (gl.x_intern_atom)(x_display, b"_NET_WM_STATE_ABOVE\0".as_ptr() as *const c_char, 1);
// 先 map 窗口kwin 才会处理
(gl.x_map_window)(x_display, x_window);
(gl.x_flush)(x_display);
(gl.x_sync)(x_display, 0);
// 发送 _NET_WM_STATE 客户端消息两个状态FULLSCREEN + ABOVE
let mut event: x11::XClientMessageEvent = std::mem::zeroed();
event.ftype = 33; // ClientMessage
event.display = x_display as *mut c_void;
event.window = x_window;
event.message_type = net_wm_state;
event.format = 32;
event.data_l[0] = 1; // _NET_WM_STATE_ADD
event.data_l[1] = net_wm_state_fs;
event.data_l[2] = net_wm_state_above;
event.data_l[3] = 1; // source indication: application
let event_mask = x11::SubstructureNotifyMask | x11::SubstructureRedirectMask;
(gl.x_send_event)(
x_display,
root_window,
0,
event_mask,
&mut event as *mut x11::XClientMessageEvent as *mut x11::XEvent,
);
(gl.x_sync)(x_display, 0);
println!("[Live2D] 已请求 kwin 全屏置顶");
}
// EGL
@@ -503,7 +601,7 @@ impl RenderContext {
}
let ctx_attrs = [egl::EGL_CONTEXT_CLIENT_VERSION, 2, egl::EGL_NONE];
let context = unsafe { (gl.egl_create_context)(display, config, std::ptr::null(), ctx_attrs.as_ptr()) };
let context = unsafe { (gl.egl_create_context)(display, config, std::ptr::null_mut(), ctx_attrs.as_ptr()) };
if context.is_null() {
return Err(anyhow!("eglCreateContext 失败"));
}
@@ -513,7 +611,7 @@ impl RenderContext {
}
unsafe {
(gl.gl_viewport)(0, 0, width, height);
(gl.gl_viewport)(0, 0, win_w, win_h);
(gl.gl_clear_color)(0.0, 0.0, 0.0, 1.0);
(gl.gl_enable)(gl::GL_BLEND);
(gl.gl_blend_func)(gl::GL_SRC_ALPHA, gl::GL_ONE_MINUS_SRC_ALPHA);
@@ -529,13 +627,10 @@ impl RenderContext {
let mut status = 0;
(gl.gl_get_programiv)(p, gl::GL_LINK_STATUS, &mut status);
if status == 0 {
let mut log = vec![0i8; 1024];
(gl.gl_get_program_info_log)(p, 1024, std::ptr::null_mut(), log.as_mut_ptr());
let log_str = String::from_utf8_lossy(std::slice::from_raw_parts(
log.as_ptr() as *const u8,
1024,
));
return Err(anyhow!("着色器链接失败: {}", log_str));
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);
let log_str = String::from_utf8_lossy(&log);
return Err(anyhow!("shader link failed: {}", log_str));
}
p
};
@@ -568,6 +663,7 @@ impl RenderContext {
gl,
display,
surface,
context,
x_display,
x_window,
program,
@@ -620,4 +716,26 @@ impl RenderContext {
pub fn clear(&self) {
unsafe { (self.gl.gl_clear)(gl::GL_COLOR_BUFFER_BIT); }
}
/// 将窗口提升到 X11 堆叠顺序的最顶层
pub fn raise_window(&self) {
unsafe {
(self.gl.x_raise_window)(self.x_display, self.x_window);
}
}
/// 将 EGL 上下文绑定到当前线程EGL 上下文是线程绑定的)
pub fn make_current(&self) -> Result<()> {
if unsafe { (self.gl.egl_make_current)(self.display, self.surface, self.surface, self.context) } == 0 {
return Err(anyhow!("eglMakeCurrent 失败(子线程)"));
}
Ok(())
}
/// 释放当前线程的 EGL 上下文绑定,允许其他线程绑定
pub fn release_current(&self) {
unsafe {
(self.gl.egl_make_current)(self.display, std::ptr::null_mut(), std::ptr::null_mut(), std::ptr::null_mut());
}
}
}

View File

@@ -75,6 +75,13 @@ impl Plugin for Live2DPlugin {
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(())
}
@@ -136,7 +143,23 @@ impl Live2DPlugin {
));
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) {

View File

@@ -1,11 +1,12 @@
//! Live2D 娓叉煋鍣細鏁村悎 Core + Model + GL + Animation
//! Live2D Renderer: Core + Model + GL + Animation integration
use super::animation::AnimationState;
use super::assets::{self, Model3Json, TextureImage};
use super::core_ffi::{self, CubismCore};
use super::gl::{self as gl_api, RenderContext};
use gl_api::gl as gl_const;
use super::model::{CubismModel, DrawableSnapshot};
use anyhow::{Context, Result};
use anyhow::Result;
use std::path::Path;
use std::sync::Arc;
use std::time::{Duration, Instant};
@@ -31,12 +32,12 @@ impl Live2DRenderer {
height: i32,
) -> Result<Self> {
let core = Arc::new(CubismCore::load(core_so_path)?);
println!("[Live2D] Core 鐗堟湰: {}", core.version_string());
println!("[Live2D] Core version: {}", core.version_string());
let paths = assets::resolve_asset_paths(live2d_base_dir, model3_json_rel)?;
let model3 = Model3Json::from_path(&paths.model3_json)?;
println!(
"[Live2D] model3.json: 鐗堟湰={} moc={} 绾圭悊鏁?{}",
"[Live2D] model3.json: ver={} moc={} textures={}",
model3.version,
model3.file_references.moc,
model3.file_references.textures.len()
@@ -48,7 +49,7 @@ impl Live2DRenderer {
let gl_ctx = RenderContext::new_fullscreen(width, height)?;
let texture_ids: Vec<u32> = textures.iter().map(|t| gl_ctx.create_texture(t)).collect();
println!("[Live2D] 宸蹭笂浼?{} 涓汗鐞嗗埌 GPU", texture_ids.len());
println!("[Live2D] Uploaded {} textures", texture_ids.len());
let canvas = model.canvas;
let model_matrix = compute_model_matrix(canvas, width, height);
@@ -84,13 +85,13 @@ impl Live2DRenderer {
unsafe {
let gl = &self.gl_ctx.gl;
(gl.gl_clear_color)(0.0, 0.0, 0.0, 1.0);
(gl.gl_clear)(gl_api::GL_COLOR_BUFFER_BIT);
(gl.gl_enable)(gl_api::GL_BLEND);
(gl.gl_clear)(gl_const::GL_COLOR_BUFFER_BIT);
(gl.gl_enable)(gl_const::GL_BLEND);
(gl.gl_use_program)(self.gl_ctx.program);
(gl.gl_uniform_matrix4fv)(
self.gl_ctx.uniform_matrix,
1,
gl_api::GL_FALSE,
gl_const::GL_FALSE,
self.model_matrix.as_ptr(),
);
}
@@ -108,44 +109,43 @@ impl Live2DRenderer {
(self.gl_ctx.gl.egl_swap_buffers)(self.gl_ctx.display, self.gl_ctx.surface);
}
// 每帧提升窗口到最顶层,防止桌面/其他窗口遮挡
self.gl_ctx.raise_window();
Ok(())
}
fn draw_drawable(&self, snap: &DrawableSnapshot) {
let gl = &self.gl_ctx.gl;
unsafe {
// 绾圭悊
let tex_idx = snap.texture_index as usize;
if tex_idx < self.texture_ids.len() {
(gl.gl_active_texture)(gl_api::GL_TEXTURE0);
(gl.gl_bind_texture)(gl_api::GL_TEXTURE_2D, self.texture_ids[tex_idx]);
(gl.gl_active_texture)(gl_const::GL_TEXTURE0);
(gl.gl_bind_texture)(gl_const::GL_TEXTURE_2D, self.texture_ids[tex_idx]);
(gl.gl_uniform1i)(self.gl_ctx.uniform_texture, 0);
}
// base color锛堜笉閫忔槑搴︿箻鍒?alpha锛? (gl.gl_uniform4f)(self.gl_ctx.uniform_base_color, 1.0, 1.0, 1.0, snap.opacity);
// multiply/screen color
(gl.gl_uniform4f)(self.gl_ctx.uniform_base_color, 1.0, 1.0, 1.0, snap.opacity);
let mc = snap.multiply_color;
(gl.gl_uniform4f)(self.gl_ctx.uniform_multiply_color, mc[0], mc[1], mc[2], mc[3]);
let sc = snap.screen_color;
(gl.gl_uniform4f)(self.gl_ctx.uniform_screen_color, sc[0], sc[1], sc[2], sc[3]);
// 娣峰悎妯″紡
match snap.blend_mode {
core_ffi::blend_mode::NORMAL => {
(gl.gl_blend_func)(gl_api::GL_SRC_ALPHA, gl_api::GL_ONE_MINUS_SRC_ALPHA);
(gl.gl_blend_func)(gl_const::GL_SRC_ALPHA, gl_const::GL_ONE_MINUS_SRC_ALPHA);
}
core_ffi::blend_mode::ADDITIVE => {
(gl.gl_blend_func)(gl_api::GL_SRC_ALPHA, gl_api::GL_ONE);
(gl.gl_blend_func)(gl_const::GL_SRC_ALPHA, gl_const::GL_ONE);
}
core_ffi::blend_mode::MULTIPLICATIVE => {
(gl.gl_blend_func)(gl_api::GL_ZERO, gl_api::GL_SRC_COLOR);
(gl.gl_blend_func)(gl_const::GL_ZERO, gl_const::GL_SRC_COLOR);
}
_ => {
(gl.gl_blend_func)(gl_api::GL_SRC_ALPHA, gl_api::GL_ONE_MINUS_SRC_ALPHA);
(gl.gl_blend_func)(gl_const::GL_SRC_ALPHA, gl_const::GL_ONE_MINUS_SRC_ALPHA);
}
}
// 椤剁偣鏁版嵁锛歱osition(xy) + texCoord(uv) 浜ら敊
let vc = snap.vertex_positions.len();
let mut vertex_data: Vec<f32> = Vec::with_capacity(vc * 4);
for i in 0..vc {
@@ -157,22 +157,22 @@ impl Live2DRenderer {
let mut vbo = 0u32;
(gl.gl_gen_buffers)(1, &mut vbo);
(gl.gl_bind_buffer)(gl_api::GL_ARRAY_BUFFER, vbo);
(gl.gl_bind_buffer)(gl_const::GL_ARRAY_BUFFER, vbo);
(gl.gl_buffer_data)(
gl_api::GL_ARRAY_BUFFER,
gl_const::GL_ARRAY_BUFFER,
(vertex_data.len() * 4) as isize,
vertex_data.as_ptr() as *const std::ffi::c_void,
gl_api::GL_STATIC_DRAW,
gl_const::GL_STATIC_DRAW,
);
let mut ibo = 0u32;
(gl.gl_gen_buffers)(1, &mut ibo);
(gl.gl_bind_buffer)(gl_api::GL_ELEMENT_ARRAY_BUFFER, ibo);
(gl.gl_bind_buffer)(gl_const::GL_ELEMENT_ARRAY_BUFFER, ibo);
(gl.gl_buffer_data)(
gl_api::GL_ELEMENT_ARRAY_BUFFER,
gl_const::GL_ELEMENT_ARRAY_BUFFER,
(snap.indices.len() * 2) as isize,
snap.indices.as_ptr() as *const std::ffi::c_void,
gl_api::GL_STATIC_DRAW,
gl_const::GL_STATIC_DRAW,
);
let stride = 4 * 4;
@@ -181,8 +181,8 @@ impl Live2DRenderer {
(gl.gl_vertex_attrib_pointer)(
self.gl_ctx.attrib_position as u32,
2,
gl_api::GL_FLOAT,
gl_api::GL_FALSE,
gl_const::GL_FLOAT,
gl_const::GL_FALSE,
stride,
std::ptr::null(),
);
@@ -192,17 +192,17 @@ impl Live2DRenderer {
(gl.gl_vertex_attrib_pointer)(
self.gl_ctx.attrib_tex_coord as u32,
2,
gl_api::GL_FLOAT,
gl_api::GL_FALSE,
gl_const::GL_FLOAT,
gl_const::GL_FALSE,
stride,
(2 * 4) as *const std::ffi::c_void,
);
}
(gl.gl_draw_elements)(
gl_api::GL_TRIANGLES,
gl_const::GL_TRIANGLES,
snap.indices.len() as i32,
gl_api::GL_UNSIGNED_SHORT,
gl_const::GL_UNSIGNED_SHORT,
std::ptr::null(),
);
@@ -218,7 +218,7 @@ impl Live2DRenderer {
while self.running {
let start = Instant::now();
if let Err(e) = self.render_frame() {
eprintln!("[Live2D] 娓叉煋甯уけ璐? {e}");
eprintln!("[Live2D] render frame failed: {e}");
break;
}
let elapsed = start.elapsed();
@@ -234,8 +234,9 @@ impl Live2DRenderer {
}
}
/// 璁$畻 model 鈫?NDC 鍙樻崲鐭╅樀锛堝垪浼樺厛 4x4锛?///
/// Cubism 鍧愭爣锛氬師鐐瑰湪 canvas 涓績锛孻 杞村悜涓婏紝鍗曚綅涓哄儚绱犮€?/// 闇€瑕佺瓑姣旂缉鏀句娇妯″瀷瀹屾暣鏄剧ず鍦ㄧ獥鍙e唴銆?fn compute_model_matrix(
/// Compute model -> NDC transform matrix (column-major 4x4).
/// Cubism coords: origin at canvas center, Y up, unit = pixel.
fn compute_model_matrix(
canvas: super::model::CanvasInfo,
_width: i32,
_height: i32,
@@ -243,16 +244,10 @@ impl Live2DRenderer {
let canvas_w = canvas.size[0].max(1.0);
let canvas_h = canvas.size[1].max(1.0);
// 璁$畻缂╂斁锛氳妯″瀷绛夋瘮閫傚簲绐楀彛
let scale_x = 2.0 / canvas_w;
let scale_y = 2.0 / canvas_h;
let scale = scale_x.min(scale_y);
// 鍒椾紭鍏堢煩闃碉細
// [ scale 0 0 0 ]
// [ 0 scale 0 0 ]
// [ 0 0 1 0 ]
// [ 0 0 0 1 ]
[
scale, 0.0, 0.0, 0.0,
0.0, scale, 0.0, 0.0,

View File

@@ -118,6 +118,17 @@ impl Plugin for VideoPlugin {
.as_ref()
.context("video plugin context is not initialized")?;
// 如果是 Live2D 模式,不启动视频播放
if ctx.config.character.render_type == crate::core::config::RenderType::Live2d {
println!("[VideoPlugin] Live2D 模式,跳过视频播放启动");
ctx.tx.send(Envelope {
from: self.id().to_string(),
to: Destination::Manager,
message: Message::PluginReady(self.id().to_string()),
})?;
return Ok(());
}
let processor = Arc::new(Mutex::new(VideoProcessor::new((*ctx.config).clone())?));
let worker_processor = Arc::clone(&processor);
let tx = ctx.tx.clone();

BIN
w3.xwd Normal file

Binary file not shown.

BIN
w5.xwd Normal file

Binary file not shown.

BIN
w6.xwd Normal file

Binary file not shown.