diff --git a/src/plugins/live2d/gl.rs b/src/plugins/live2d/gl.rs index 4cb5418..9f30ff6 100644 --- a/src/plugins/live2d/gl.rs +++ b/src/plugins/live2d/gl.rs @@ -21,6 +21,8 @@ pub mod egl { pub const EGL_OPENGL_ES2_BIT: c_int = 0x0004; pub const EGL_NONE: c_int = 0x3038; pub const EGL_CONTEXT_CLIENT_VERSION: c_int = 0x3098; + pub const EGL_DEPTH_SIZE: c_int = 0x3025; + pub const EGL_STENCIL_SIZE: c_int = 0x3026; pub type EGLDisplay = *mut c_void; pub type EGLConfig = *mut c_void; @@ -94,13 +96,14 @@ pub mod x11 { // ============ GLES2 ============ pub mod gl { - use std::os::raw::{c_int, c_uint}; + use std::os::raw::{c_int, c_uint, c_uchar}; pub type GLenum = c_uint; pub type GLuint = c_uint; pub type GLint = c_int; pub type GLsizei = c_int; pub type GLbitfield = c_uint; + pub type GLboolean = c_uchar; pub const GL_FLOAT: GLenum = 0x1406; pub const GL_ARRAY_BUFFER: GLenum = 0x8892; @@ -121,6 +124,7 @@ pub mod gl { pub const GL_COMPILE_STATUS: GLenum = 0x8B81; pub const GL_LINK_STATUS: GLenum = 0x8B82; pub const GL_TRIANGLES: GLenum = 0x0004; + pub const GL_TRIANGLE_STRIP: GLenum = 0x0005; pub const GL_BLEND: GLenum = 0x0BE2; pub const GL_SRC_ALPHA: GLenum = 0x0302; pub const GL_ONE_MINUS_SRC_ALPHA: GLenum = 0x0303; @@ -131,6 +135,20 @@ pub mod gl { pub const GL_STATIC_DRAW: GLenum = 0x88E4; pub const GL_FALSE: GLenum = 0; pub const GL_NO_ERROR: GLenum = 0; + + // 遮罩(stencil)相关常量 + pub const GL_DEPTH_TEST: GLenum = 0x0B71; + pub const GL_STENCIL_TEST: GLenum = 0x0B90; + pub const GL_STENCIL_BUFFER_BIT: GLbitfield = 0x0400; + pub const GL_KEEP: GLenum = 0x1E00; + pub const GL_REPLACE: GLenum = 0x1E01; + pub const GL_INCR: GLenum = 0x1E02; + pub const GL_DECR: GLenum = 0x1E03; + pub const GL_ALWAYS: GLenum = 0x0207; + pub const GL_EQUAL: GLenum = 0x0202; + pub const GL_NOTEQUAL: GLenum = 0x0205; + pub const GL_LESS: GLenum = 0x0201; + pub const GL_TRUE: GLboolean = 1; } pub use gl::{GLenum, GLuint, GLint, GLsizei}; @@ -161,7 +179,59 @@ void main() { texColor.rgb = texColor.rgb * u_multiplyColor.rgb; texColor.rgb = texColor.rgb + u_screenColor.rgb - (texColor.rgb * u_screenColor.rgb); vec4 color = texColor * u_baseColor; - gl_FragColor = vec4(color.rgb * color.a, color.a); + gl_FragColor = vec4(color.rgb, color.a); +} +"#; + +/// 背景渐变着色器(全屏四边形,从底部到底部到顶部做线性插值) +pub const GRAD_VERT_SHADER_SRC: &str = r#"#version 100 +attribute vec2 a_pos; +varying float v_y; +void main() { + v_y = a_pos.y; + gl_Position = vec4(a_pos, 0.0, 1.0); +} +"#; + +pub const GRAD_FRAG_SHADER_SRC: &str = r#"#version 100 +precision highp float; +varying float v_y; +uniform vec4 u_top; +uniform vec4 u_bottom; +void main() { + float t = (v_y + 1.0) * 0.5; + vec3 c = mix(u_bottom.rgb, u_top.rgb, t); + gl_FragColor = vec4(c, 1.0); +} +"#; + +/// 遮罩(mask)着色器:仅把 mask drawable 的不透明区域写入 stencil, +/// 用于裁切被遮罩的 drawable(如夹在身体里的后臂、被头发遮挡的脸)。 +pub const MASK_VERT_SHADER_SRC: &str = r#"#version 100 +attribute vec4 a_position; +attribute vec2 a_texCoord; +varying vec2 v_texCoord; +uniform mat4 u_matrix; +void main() { + gl_Position = u_matrix * a_position; + v_texCoord = a_texCoord; + v_texCoord.y = 1.0 - v_texCoord.y; +} +"#; + +pub const MASK_FRAG_SHADER_SRC: &str = r#"#version 100 +precision highp float; +varying vec2 v_texCoord; +uniform sampler2D s_texture0; +uniform vec4 u_multiplyColor; +uniform vec4 u_screenColor; +void main() { + vec4 c = texture2D(s_texture0, v_texCoord); + c.rgb = c.rgb * u_multiplyColor.rgb; + c.rgb = c.rgb + u_screenColor.rgb - (c.rgb * u_screenColor.rgb); + // 仅不透明区域写入 stencil(裁切形状由 alpha 决定) + if (c.a < 0.5) discard; + gl_FragColor = vec4(1.0); } "#; @@ -209,6 +279,7 @@ pub struct Gl { pub gl_clear: unsafe extern "C" fn(mask: gl::GLbitfield), pub gl_clear_color: unsafe extern "C" fn(r: f32, g: f32, b: f32, a: f32), pub gl_enable: unsafe extern "C" fn(cap: gl::GLenum), + pub gl_disable: unsafe extern "C" fn(cap: gl::GLenum), pub gl_blend_func: unsafe extern "C" fn(sfactor: gl::GLenum, dfactor: gl::GLenum), pub gl_viewport: unsafe extern "C" fn(x: gl::GLint, y: gl::GLint, w: gl::GLsizei, h: gl::GLsizei), pub gl_create_shader: unsafe extern "C" fn(t: gl::GLenum) -> gl::GLuint, @@ -290,8 +361,18 @@ pub struct Gl { type_: gl::GLenum, indices: *const c_void_raw, ), + pub gl_draw_arrays: unsafe extern "C" fn(mode: gl::GLenum, first: gl::GLint, count: gl::GLsizei), pub gl_get_error: unsafe extern "C" fn() -> gl::GLenum, pub gl_delete_buffers: unsafe extern "C" fn(n: gl::GLsizei, buffers: *const gl::GLuint), + pub gl_stencil_func: unsafe extern "C" fn(func: gl::GLenum, ref_: gl::GLint, mask: gl::GLuint), + pub gl_stencil_op: unsafe extern "C" fn(fail: gl::GLenum, zfail: gl::GLenum, zpass: gl::GLenum), + pub gl_stencil_mask: unsafe extern "C" fn(mask: gl::GLuint), + pub gl_color_mask: unsafe extern "C" fn( + r: gl::GLboolean, + g: gl::GLboolean, + b: gl::GLboolean, + a: gl::GLboolean, + ), pub x_open_display: unsafe extern "C" fn(name: *const c_char) -> *mut x11::Display, pub x_create_window: unsafe extern "C" fn( @@ -360,6 +441,7 @@ impl Gl { gl_clear: *lib_gl.get(b"glClear\0")?, gl_clear_color: *lib_gl.get(b"glClearColor\0")?, gl_enable: *lib_gl.get(b"glEnable\0")?, + gl_disable: *lib_gl.get(b"glDisable\0")?, gl_blend_func: *lib_gl.get(b"glBlendFunc\0")?, gl_viewport: *lib_gl.get(b"glViewport\0")?, gl_create_shader: *lib_gl.get(b"glCreateShader\0")?, @@ -389,8 +471,13 @@ impl Gl { gl_enable_vertex_attrib_array: *lib_gl.get(b"glEnableVertexAttribArray\0")?, gl_vertex_attrib_pointer: *lib_gl.get(b"glVertexAttribPointer\0")?, gl_draw_elements: *lib_gl.get(b"glDrawElements\0")?, + gl_draw_arrays: *lib_gl.get(b"glDrawArrays\0")?, gl_get_error: *lib_gl.get(b"glGetError\0")?, gl_delete_buffers: *lib_gl.get(b"glDeleteBuffers\0")?, + gl_stencil_func: *lib_gl.get(b"glStencilFunc\0")?, + gl_stencil_op: *lib_gl.get(b"glStencilOp\0")?, + gl_stencil_mask: *lib_gl.get(b"glStencilMask\0")?, + gl_color_mask: *lib_gl.get(b"glColorMask\0")?, x_open_display: *lib_x11.get(b"XOpenDisplay\0")?, x_create_window: *lib_x11.get(b"XCreateWindow\0")?, @@ -431,6 +518,18 @@ pub struct RenderContext { pub uniform_base_color: GLint, pub uniform_multiply_color: GLint, pub uniform_screen_color: GLint, + pub grad_program: GLuint, + pub grad_attrib_pos: GLint, + pub grad_uniform_top: GLint, + pub grad_uniform_bottom: GLint, + // 遮罩(mask)程序 + pub mask_program: GLuint, + pub mask_attrib_position: GLint, + pub mask_attrib_tex_coord: GLint, + pub mask_uniform_matrix: GLint, + pub mask_uniform_texture: GLint, + pub mask_uniform_multiply_color: GLint, + pub mask_uniform_screen_color: GLint, pub width: i32, pub height: i32, } @@ -476,11 +575,12 @@ impl RenderContext { 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); + // 全屏渲染:直接使用显示器实际分辨率创建窗口与 EGL surface,避免 + // 「帧缓冲 800x800 被放进 1920x1080 窗口、模型只出现在左下角」的问题。 + // 若 X11 取不到屏幕分辨率,再退回传入的 width/height。 + let win_w = if screen_w > 0 && screen_h > 0 { screen_w } else { width }; + let win_h = if screen_w > 0 && screen_h > 0 { screen_h } else { height }; + println!("[Live2D] 窗口尺寸: {}x{} (屏幕实际 {}x{})", win_w, win_h, screen_w, screen_h); let mut attrs = x11::XSetWindowAttributes { background_pixmap: 0, @@ -569,7 +669,20 @@ impl RenderContext { } println!("[Live2D] EGL {}.{}", major, minor); + // 主配置请求 depth+stencil(Live2D 遮罩需要 stencil); + // 若设备不支持该组合,退回不带 depth/stencil 的配置。 let config_attrs = [ + egl::EGL_RED_SIZE, 8, + egl::EGL_GREEN_SIZE, 8, + egl::EGL_BLUE_SIZE, 8, + egl::EGL_ALPHA_SIZE, 8, + egl::EGL_SURFACE_TYPE, egl::EGL_WINDOW_BIT, + egl::EGL_RENDERABLE_TYPE, egl::EGL_OPENGL_ES2_BIT, + egl::EGL_DEPTH_SIZE, 16, + egl::EGL_STENCIL_SIZE, 8, + egl::EGL_NONE, + ]; + let config_attrs_fallback = [ egl::EGL_RED_SIZE, 8, egl::EGL_GREEN_SIZE, 8, egl::EGL_BLUE_SIZE, 8, @@ -580,18 +693,26 @@ impl RenderContext { ]; let mut config: egl::EGLConfig = std::ptr::null_mut(); let mut num_config = 0; - if unsafe { - (gl.egl_choose_config)( - display, - config_attrs.as_ptr(), - &mut config, - 1, - &mut num_config, - ) - } == 0 || num_config == 0 - { + let mut choose = |attrs: &[c_int]| -> bool { + let mut cfg: egl::EGLConfig = std::ptr::null_mut(); + let mut n = 0; + let ok = unsafe { + (gl.egl_choose_config)(display, attrs.as_ptr(), &mut cfg, 1, &mut n) + } != 0 && n > 0; + if ok { + config = cfg; + num_config = n; + } + ok + }; + if !choose(&config_attrs) && !choose(&config_attrs_fallback) { return Err(anyhow!("eglChooseConfig 失败")); } + println!( + "[Live2D] EGL 配置选定: 含stencil={} (num_config={})", + config_attrs.len() > 9, + num_config + ); let surface = unsafe { (gl.egl_create_window_surface)(display, config, x_window as egl::NativeWindowType, std::ptr::null()) @@ -653,11 +774,87 @@ impl RenderContext { let uniform_multiply_color = getu("u_multiplyColor")?; let uniform_screen_color = getu("u_screenColor")?; + // 背景渐变程序 + let grad_vert = compile_shader(&gl, gl::GL_VERTEX_SHADER, GRAD_VERT_SHADER_SRC)?; + let grad_frag = compile_shader(&gl, gl::GL_FRAGMENT_SHADER, GRAD_FRAG_SHADER_SRC)?; + let grad_program = unsafe { + let p = (gl.gl_create_program)(); + (gl.gl_attach_shader)(p, grad_vert); + (gl.gl_attach_shader)(p, grad_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 = 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!("gradient shader link failed: {}", String::from_utf8_lossy(&log))); + } + p + }; + let grad_attrib_pos = { + let c = std::ffi::CString::new("a_pos")?; + unsafe { (gl.gl_get_attrib_location)(grad_program, c.as_ptr()) } + }; + let grad_uniform_top = { + let c = std::ffi::CString::new("u_top")?; + unsafe { (gl.gl_get_uniform_location)(grad_program, c.as_ptr()) } + }; + let grad_uniform_bottom = { + let c = std::ffi::CString::new("u_bottom")?; + unsafe { (gl.gl_get_uniform_location)(grad_program, c.as_ptr()) } + }; + + // 遮罩(mask)程序:仅用于把 mask drawable 写入 stencil + let mask_vert = compile_shader(&gl, gl::GL_VERTEX_SHADER, MASK_VERT_SHADER_SRC)?; + let mask_frag = compile_shader(&gl, gl::GL_FRAGMENT_SHADER, MASK_FRAG_SHADER_SRC)?; + let mask_program = unsafe { + let p = (gl.gl_create_program)(); + (gl.gl_attach_shader)(p, mask_vert); + (gl.gl_attach_shader)(p, mask_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 = 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!("mask shader link failed: {}", String::from_utf8_lossy(&log))); + } + p + }; + let mask_attrib_position = { + let c = std::ffi::CString::new("a_position")?; + unsafe { (gl.gl_get_attrib_location)(mask_program, c.as_ptr()) } + }; + let mask_attrib_tex_coord = { + let c = std::ffi::CString::new("a_texCoord")?; + unsafe { (gl.gl_get_attrib_location)(mask_program, c.as_ptr()) } + }; + let mask_uniform_matrix = { + let c = std::ffi::CString::new("u_matrix")?; + unsafe { (gl.gl_get_uniform_location)(mask_program, c.as_ptr()) } + }; + let mask_uniform_texture = { + let c = std::ffi::CString::new("s_texture0")?; + unsafe { (gl.gl_get_uniform_location)(mask_program, c.as_ptr()) } + }; + let mask_uniform_multiply_color = { + let c = std::ffi::CString::new("u_multiplyColor")?; + unsafe { (gl.gl_get_uniform_location)(mask_program, c.as_ptr()) } + }; + let mask_uniform_screen_color = { + let c = std::ffi::CString::new("u_screenColor")?; + unsafe { (gl.gl_get_uniform_location)(mask_program, c.as_ptr()) } + }; + println!( "[Live2D] GL 程序就绪: program={} pos={} tex={} mat={} tex_u={} base={} mult={} scr={}", program, attrib_position, attrib_tex_coord, uniform_matrix, uniform_texture, uniform_base_color, uniform_multiply_color, uniform_screen_color ); + println!( + "[Live2D] 渐变程序就绪: grad={} pos={} top={} bottom={}", + grad_program, grad_attrib_pos, grad_uniform_top, grad_uniform_bottom + ); Ok(Self { gl, @@ -674,8 +871,19 @@ impl RenderContext { uniform_base_color, uniform_multiply_color, uniform_screen_color, - width, - height, + grad_program, + grad_attrib_pos, + grad_uniform_top, + grad_uniform_bottom, + mask_program, + mask_attrib_position, + mask_attrib_tex_coord, + mask_uniform_matrix, + mask_uniform_texture, + mask_uniform_multiply_color, + mask_uniform_screen_color, + width: win_w, + height: win_h, }) } @@ -713,6 +921,47 @@ impl RenderContext { } } + /// 绘制全屏渐变背景(在模型之前调用)。top/bottom 为 RGB,范围 0..1。 + pub fn draw_background(&self, top: [f32; 3], bottom: [f32; 3]) { + unsafe { + // 背景不透明,先关闭混合 + (self.gl.gl_disable)(gl::GL_BLEND); + (self.gl.gl_use_program)(self.grad_program); + + let verts: [f32; 8] = [ + -1.0, -1.0, + 1.0, -1.0, + -1.0, 1.0, + 1.0, 1.0, + ]; + let mut vbo = 0; + (self.gl.gl_gen_buffers)(1, &mut vbo); + (self.gl.gl_bind_buffer)(gl::GL_ARRAY_BUFFER, vbo); + (self.gl.gl_buffer_data)( + gl::GL_ARRAY_BUFFER, + (verts.len() * 4) as isize, + verts.as_ptr() as *const c_void, + gl::GL_STATIC_DRAW, + ); + (self.gl.gl_enable_vertex_attrib_array)(self.grad_attrib_pos as u32); + (self.gl.gl_vertex_attrib_pointer)( + self.grad_attrib_pos as u32, + 2, + gl::GL_FLOAT, + gl::GL_FALSE, + 0, + std::ptr::null(), + ); + (self.gl.gl_uniform4f)(self.grad_uniform_top, top[0], top[1], top[2], 1.0); + (self.gl.gl_uniform4f)(self.grad_uniform_bottom, bottom[0], bottom[1], bottom[2], 1.0); + (self.gl.gl_draw_arrays)(gl::GL_TRIANGLE_STRIP, 0, 4); + (self.gl.gl_delete_buffers)(1, &vbo); + + // 恢复混合,供后续模型绘制 + (self.gl.gl_enable)(gl::GL_BLEND); + } + } + pub fn clear(&self) { unsafe { (self.gl.gl_clear)(gl::GL_COLOR_BUFFER_BIT); } } diff --git a/src/plugins/live2d/model.rs b/src/plugins/live2d/model.rs index 7b3d94e..8b7a584 100644 --- a/src/plugins/live2d/model.rs +++ b/src/plugins/live2d/model.rs @@ -11,6 +11,8 @@ use std::path::Path; /// 一个 drawable 的渲染所需数据快照 #[derive(Clone)] pub struct DrawableSnapshot { + /// 在 Core drawable 数组中的原始索引(用于查 mask 引用) + pub index: usize, pub texture_index: i32, pub draw_order: i32, pub render_order: i32, @@ -23,6 +25,8 @@ pub struct DrawableSnapshot { pub indices: Vec, pub multiply_color: [f32; 4], pub screen_color: [f32; 4], + /// 本 drawable 依赖的 mask drawable 索引(空表示无遮罩) + pub mask_indices: Vec, pub mask_counts: i32, } @@ -148,8 +152,8 @@ impl CubismModel { }; println!( - "[Live2D] 模型加载成功: 参数={} drawable={} canvas={:?}x{:.0}ppu", - param_count, drawable_count, canvas.size, canvas.pixels_per_unit + "[Live2D] 模型加载成功: 参数={} drawable={} canvas={:?} origin=({:.1},{:.1})x{:.0}ppu", + param_count, drawable_count, canvas.size, canvas.origin[0], canvas.origin[1], canvas.pixels_per_unit ); Ok(Self { @@ -254,6 +258,7 @@ impl CubismModel { let indices = (core.csm_get_drawable_indices)(self.model); let multiply_colors = (core.csm_get_drawable_multiply_colors)(self.model); let screen_colors = (core.csm_get_drawable_screen_colors)(self.model); + let masks = (core.csm_get_drawable_masks)(self.model); // 先收集每个 drawable 的原始索引 let mut items: Vec<(usize, i32)> = (0..count) @@ -287,7 +292,18 @@ impl CubismModel { let mc = *multiply_colors.add(i); let sc = *screen_colors.add(i); + // 取出本 drawable 的 mask 索引列表 + let mk = *mask_counts.add(i) as usize; + let mut mask_indices: Vec = Vec::with_capacity(mk); + if mk > 0 { + let mptr = *masks.add(i); + for k in 0..mk { + mask_indices.push(*mptr.add(k)); + } + } + DrawableSnapshot { + index: i, texture_index: *texture_indices.add(i), draw_order: *draw_orders.add(i), render_order: ro, @@ -300,7 +316,8 @@ impl CubismModel { indices: idx_vec, multiply_color: [mc.x, mc.y, mc.z, mc.w], screen_color: [sc.x, sc.y, sc.z, sc.w], - mask_counts: *mask_counts.add(i), + mask_indices, + mask_counts: mk as i32, } }) .collect() diff --git a/src/plugins/live2d/renderer.rs b/src/plugins/live2d/renderer.rs index 56c7a10..de5d820 100644 --- a/src/plugins/live2d/renderer.rs +++ b/src/plugins/live2d/renderer.rs @@ -52,7 +52,8 @@ impl Live2DRenderer { println!("[Live2D] Uploaded {} textures", texture_ids.len()); let canvas = model.canvas; - let model_matrix = compute_model_matrix(canvas, width, height); + // 用真实窗口尺寸(已被 gl_ctx 修正为显示器实际分辨率)计算矩阵 + let model_matrix = compute_model_matrix(canvas, gl_ctx.width, gl_ctx.height); model.reset_to_default(&core); @@ -84,8 +85,16 @@ impl Live2DRenderer { unsafe { let gl = &self.gl_ctx.gl; + // 每帧校正 viewport 为窗口实际尺寸,避免窗口被 WM 调整大小后 + // 仍用旧的小尺寸(如 800x800)绘制,导致模型只出现在左下角。 + (gl.gl_viewport)(0, 0, self.gl_ctx.width, self.gl_ctx.height); (gl.gl_clear_color)(0.0, 0.0, 0.0, 1.0); (gl.gl_clear)(gl_const::GL_COLOR_BUFFER_BIT); + // 先画渐变背景,再画模型(draw_background 内部会恢复 BLEND) + self.gl_ctx.draw_background( + [0.05, 0.09, 0.17], // 顶部:深蓝(全息风) + [0.01, 0.02, 0.04], // 底部:近黑 + ); (gl.gl_enable)(gl_const::GL_BLEND); (gl.gl_use_program)(self.gl_ctx.program); (gl.gl_uniform_matrix4fv)( @@ -96,11 +105,19 @@ impl Live2DRenderer { ); } + // 建立 原始索引 -> 快照 的查找表,供被遮罩 drawable 引用其 mask + let snap_by_index: std::collections::HashMap = + snapshots.iter().map(|s| (s.index, s)).collect(); + for snap in &snapshots { if snap.opacity <= 0.001 { continue; } - self.draw_drawable(snap); + if snap.mask_indices.is_empty() { + self.draw_drawable(snap); + } else { + self.draw_masked_drawable(snap, &snap_by_index); + } } self.model.reset_dynamic_flags(&self.core); @@ -211,6 +228,147 @@ impl Live2DRenderer { } } + /// 绘制一个被遮罩(masked)的 drawable: + /// 先用 mask 着色器把所有 mask drawable 写入 stencil 缓冲,再以 stencil 裁切 + /// 实际 drawable,从而把后臂/头发等限制在 mask 形状内、不再整块溢出。 + fn draw_masked_drawable( + &self, + snap: &DrawableSnapshot, + snap_by_index: &std::collections::HashMap, + ) { + let gl = &self.gl_ctx.gl; + unsafe { + // 1) 用 mask 程序把 mask drawable 形状写入 stencil(颜色关闭) + (gl.gl_enable)(gl_const::GL_STENCIL_TEST); + (gl.gl_stencil_mask)(0xFF); + (gl.gl_clear)(gl_const::GL_STENCIL_BUFFER_BIT); + (gl.gl_color_mask)(0, 0, 0, 0); + (gl.gl_stencil_func)(gl_const::GL_ALWAYS, 1, 0xFF); + (gl.gl_stencil_op)(gl_const::GL_KEEP, gl_const::GL_KEEP, gl_const::GL_REPLACE); + + (gl.gl_use_program)(self.gl_ctx.mask_program); + (gl.gl_uniform_matrix4fv)( + self.gl_ctx.mask_uniform_matrix, + 1, + gl_const::GL_FALSE, + self.model_matrix.as_ptr(), + ); + for &mi in &snap.mask_indices { + if let Some(m) = snap_by_index.get(&(mi as usize)) { + self.draw_mask_drawable(m); + } + } + + // 2) 用 stencil 裁切实际 drawable + (gl.gl_color_mask)(1, 1, 1, 1); + (gl.gl_stencil_op)(gl_const::GL_KEEP, gl_const::GL_KEEP, gl_const::GL_KEEP); + let inverted = + (snap.constant_flags & super::core_ffi::constant_flags::IS_INVERTED_MASK) != 0; + if inverted { + (gl.gl_stencil_func)(gl_const::GL_EQUAL, 0, 0xFF); + } else { + (gl.gl_stencil_func)(gl_const::GL_NOTEQUAL, 0, 0xFF); + } + + (gl.gl_use_program)(self.gl_ctx.program); + self.draw_drawable(snap); + + // 复位,避免影响后续无遮罩 drawable + (gl.gl_disable)(gl_const::GL_STENCIL_TEST); + (gl.gl_stencil_mask)(0xFF); + } + } + + /// 用 mask 着色器把一个 drawable 画进 stencil(颜色关闭,仅 alpha 形状)。 + fn draw_mask_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_const::GL_TEXTURE0); + (gl.gl_bind_texture)(gl_const::GL_TEXTURE_2D, self.texture_ids[tex_idx]); + (gl.gl_uniform1i)(self.gl_ctx.mask_uniform_texture, 0); + } + (gl.gl_uniform4f)( + self.gl_ctx.mask_uniform_multiply_color, + snap.multiply_color[0], + snap.multiply_color[1], + snap.multiply_color[2], + snap.multiply_color[3], + ); + (gl.gl_uniform4f)( + self.gl_ctx.mask_uniform_screen_color, + snap.screen_color[0], + snap.screen_color[1], + snap.screen_color[2], + snap.screen_color[3], + ); + + let vc = snap.vertex_positions.len(); + let mut vertex_data: Vec = Vec::with_capacity(vc * 4); + for i in 0..vc { + vertex_data.push(snap.vertex_positions[i][0]); + vertex_data.push(snap.vertex_positions[i][1]); + vertex_data.push(snap.vertex_uvs[i][0]); + vertex_data.push(snap.vertex_uvs[i][1]); + } + + let mut vbo = 0u32; + (gl.gl_gen_buffers)(1, &mut vbo); + (gl.gl_bind_buffer)(gl_const::GL_ARRAY_BUFFER, vbo); + (gl.gl_buffer_data)( + gl_const::GL_ARRAY_BUFFER, + (vertex_data.len() * 4) as isize, + vertex_data.as_ptr() as *const std::ffi::c_void, + gl_const::GL_STATIC_DRAW, + ); + + let mut ibo = 0u32; + (gl.gl_gen_buffers)(1, &mut ibo); + (gl.gl_bind_buffer)(gl_const::GL_ELEMENT_ARRAY_BUFFER, ibo); + (gl.gl_buffer_data)( + gl_const::GL_ELEMENT_ARRAY_BUFFER, + (snap.indices.len() * 2) as isize, + snap.indices.as_ptr() as *const std::ffi::c_void, + gl_const::GL_STATIC_DRAW, + ); + + let stride = 4 * 4; + if self.gl_ctx.mask_attrib_position >= 0 { + (gl.gl_enable_vertex_attrib_array)(self.gl_ctx.mask_attrib_position as u32); + (gl.gl_vertex_attrib_pointer)( + self.gl_ctx.mask_attrib_position as u32, + 2, + gl_const::GL_FLOAT, + gl_const::GL_FALSE, + stride, + std::ptr::null(), + ); + } + if self.gl_ctx.mask_attrib_tex_coord >= 0 { + (gl.gl_enable_vertex_attrib_array)(self.gl_ctx.mask_attrib_tex_coord as u32); + (gl.gl_vertex_attrib_pointer)( + self.gl_ctx.mask_attrib_tex_coord as u32, + 2, + gl_const::GL_FLOAT, + gl_const::GL_FALSE, + stride, + (2 * 4) as *const std::ffi::c_void, + ); + } + + (gl.gl_draw_elements)( + gl_const::GL_TRIANGLES, + snap.indices.len() as i32, + gl_const::GL_UNSIGNED_SHORT, + std::ptr::null(), + ); + + (gl.gl_delete_buffers)(1, &vbo); + (gl.gl_delete_buffers)(1, &ibo); + } + } + pub fn run_loop(&mut self) -> Result<()> { self.running = true; let frame_duration = Duration::from_millis(16); @@ -235,22 +393,36 @@ impl Live2DRenderer { } /// Compute model -> NDC transform matrix (column-major 4x4). -/// Cubism coords: origin at canvas center, Y up, unit = pixel. +/// +/// `csmGetDrawableVertexPositions` 返回的顶点位于**模型单位**坐标系,范围约为 +/// ±(canvas_w/2 / ppu)。`pixels_per_unit`(ppu) 指明 1 模型单位 = ppu 个画布像素。 +/// 因此顶点必须先乘 `ppu` 回到画布像素,再映射到 [-1,1] 裁剪空间。 +/// +/// 实测 Haru 的 `origin = (canvas_w/2, canvas_h/2)`,即模型坐标原点本身就位于 +/// 画布中心,所以**无需再做原点平移**(平移反而会把模型推出屏幕)。Cubism 顶点 +/// 以画布中心为原点、Y 轴向上,与裁剪空间一致,故无需平移或翻转。 +/// +/// 适配策略:**contain(整只角色恰好放入视口)**,单一均匀缩放即可。 +/// NDC 立方体由视口自动拉伸到窗口,因此**绝对不能**对 x/y 轴分别乘不同系数, +/// 否则会破坏长宽比(手臂被拉变形)。这里只按画布较长边归一,使整只角色完整 +/// 可见、不裁切、不拉伸;窗口为横屏时角色会填满高度、两侧留渐变背景。 fn compute_model_matrix( canvas: super::model::CanvasInfo, _width: i32, _height: i32, ) -> [f32; 16] { - let canvas_w = canvas.size[0].max(1.0); - let canvas_h = canvas.size[1].max(1.0); + let cw = canvas.size[0].max(1.0); + let ch = canvas.size[1].max(1.0); + let ppu = canvas.pixels_per_unit.max(1.0); - let scale_x = 2.0 / canvas_w; - let scale_y = 2.0 / canvas_h; - let scale = scale_x.min(scale_y); + // 均匀缩放:按画布较长边把整只角色映射到裁剪空间(margin=1.0 即恰好贴满, + // 若想留一点边距可改小,如 0.95)。 + let margin = 1.0; + let u = 2.0 * ppu * margin / cw.max(ch); [ - scale, 0.0, 0.0, 0.0, - 0.0, scale, 0.0, 0.0, + u, 0.0, 0.0, 0.0, + 0.0, u, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, ]