feat(live2d): 添加 Pose 处理模块以管理互斥 Part 的显示状态,优化模型加载和参数更新
This commit is contained in:
@@ -91,6 +91,8 @@ pub struct CubismCore {
|
||||
unsafe extern "C" fn(model: *const c_void) -> *const f32,
|
||||
pub csm_get_parameter_values: unsafe extern "C" fn(model: *mut c_void) -> *mut f32,
|
||||
pub csm_get_part_count: unsafe extern "C" fn(model: *const c_void) -> c_int,
|
||||
pub csm_get_part_ids:
|
||||
unsafe extern "C" fn(model: *const c_void) -> *const *const c_char,
|
||||
pub csm_get_part_opacities: unsafe extern "C" fn(model: *mut c_void) -> *mut f32,
|
||||
pub csm_get_drawable_count: unsafe extern "C" fn(model: *const c_void) -> c_int,
|
||||
pub csm_get_drawable_ids:
|
||||
@@ -163,6 +165,7 @@ impl CubismCore {
|
||||
.get(b"csmGetParameterDefaultValues\0")?,
|
||||
csm_get_parameter_values: *lib.get(b"csmGetParameterValues\0")?,
|
||||
csm_get_part_count: *lib.get(b"csmGetPartCount\0")?,
|
||||
csm_get_part_ids: *lib.get(b"csmGetPartIds\0")?,
|
||||
csm_get_part_opacities: *lib.get(b"csmGetPartOpacities\0")?,
|
||||
csm_get_drawable_count: *lib.get(b"csmGetDrawableCount\0")?,
|
||||
csm_get_drawable_ids: *lib.get(b"csmGetDrawableIds\0")?,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
//! - `assets`:model3.json 解析、PNG 纹理加载
|
||||
//! - `gl`:EGL 上下文 + OpenGL ES 2.0 着色器/渲染
|
||||
//! - `animation`:参数动画(眨眼、嘴部、待机呼吸)
|
||||
//! - `pose`:Pose PartOpacity 切换(避免互斥手臂/服装同时显示)
|
||||
//! - `renderer`:整合渲染循环
|
||||
|
||||
pub mod animation;
|
||||
@@ -13,6 +14,7 @@ pub mod assets;
|
||||
pub mod core_ffi;
|
||||
pub mod gl;
|
||||
pub mod model;
|
||||
pub mod pose;
|
||||
pub mod renderer;
|
||||
|
||||
mod plugin;
|
||||
|
||||
@@ -64,6 +64,12 @@ pub struct CubismModel {
|
||||
pub param_ids: Vec<String>,
|
||||
/// 参数索引映射(id → index)
|
||||
pub param_index: std::collections::HashMap<String, usize>,
|
||||
/// Part 数量
|
||||
pub part_count: usize,
|
||||
/// Part ID 列表(与 Core 内部顺序一致)
|
||||
pub part_ids: Vec<String>,
|
||||
/// Part 索引映射(id → index)
|
||||
pub part_index: std::collections::HashMap<String, usize>,
|
||||
/// drawable 数量
|
||||
pub drawable_count: usize,
|
||||
/// canvas 信息
|
||||
@@ -134,6 +140,20 @@ impl CubismModel {
|
||||
param_ids.push(id);
|
||||
}
|
||||
|
||||
// 读取 part 信息(Pose 会按 PartOpacity 切换互斥部件,如 Haru 的两组手臂)
|
||||
let part_count = unsafe { (core.csm_get_part_count)(model) } as usize;
|
||||
let part_ids_ptr = unsafe { (core.csm_get_part_ids)(model) };
|
||||
let mut part_ids = Vec::with_capacity(part_count);
|
||||
let mut part_index = std::collections::HashMap::new();
|
||||
for i in 0..part_count {
|
||||
let id = unsafe {
|
||||
super::core_ffi::cstr_ptr_to_string(*part_ids_ptr.add(i))
|
||||
.unwrap_or_default()
|
||||
};
|
||||
part_index.insert(id.clone(), i);
|
||||
part_ids.push(id);
|
||||
}
|
||||
|
||||
// 读取 drawable 数量
|
||||
let drawable_count = unsafe { (core.csm_get_drawable_count)(model) } as usize;
|
||||
|
||||
@@ -152,8 +172,8 @@ impl CubismModel {
|
||||
};
|
||||
|
||||
println!(
|
||||
"[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
|
||||
"[Live2D] 模型加载成功: 参数={} part={} drawable={} canvas={:?} origin=({:.1},{:.1})x{:.0}ppu",
|
||||
param_count, part_count, drawable_count, canvas.size, canvas.origin[0], canvas.origin[1], canvas.pixels_per_unit
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
@@ -165,6 +185,9 @@ impl CubismModel {
|
||||
param_count,
|
||||
param_ids,
|
||||
param_index,
|
||||
part_count,
|
||||
part_ids,
|
||||
part_index,
|
||||
drawable_count,
|
||||
canvas,
|
||||
})
|
||||
@@ -205,19 +228,34 @@ impl CubismModel {
|
||||
/// 按参数 ID 设置值
|
||||
pub fn set_param(&self, core: &CubismCore, id: &str, value: f32) -> bool {
|
||||
if let Some(&idx) = self.param_index.get(id) {
|
||||
let slice = self.param_values_mut(core);
|
||||
slice[idx] = value;
|
||||
self.set_param_by_index(core, idx, value);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// 按参数索引设置值
|
||||
pub fn set_param_by_index(&self, core: &CubismCore, index: usize, value: f32) {
|
||||
if index < self.param_count {
|
||||
let slice = self.param_values_mut(core);
|
||||
slice[index] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// 按参数 ID 获取值
|
||||
pub fn get_param(&self, core: &CubismCore, id: &str) -> Option<f32> {
|
||||
let idx = *self.param_index.get(id)?;
|
||||
self.get_param_by_index(core, idx)
|
||||
}
|
||||
|
||||
/// 按参数索引获取值
|
||||
pub fn get_param_by_index(&self, core: &CubismCore, index: usize) -> Option<f32> {
|
||||
if index >= self.param_count {
|
||||
return None;
|
||||
}
|
||||
let slice = self.param_values_mut(core);
|
||||
Some(slice[idx])
|
||||
Some(slice[index])
|
||||
}
|
||||
|
||||
/// 将所有参数重置为默认值
|
||||
@@ -229,6 +267,36 @@ impl CubismModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取 PartOpacity 缓冲区(可读写)
|
||||
pub fn part_opacities_mut(&self, core: &CubismCore) -> &mut [f32] {
|
||||
unsafe {
|
||||
let p = (core.csm_get_part_opacities)(self.model);
|
||||
std::slice::from_raw_parts_mut(p, self.part_count)
|
||||
}
|
||||
}
|
||||
|
||||
/// 按 Part ID 获取索引
|
||||
pub fn part_index(&self, id: &str) -> Option<usize> {
|
||||
self.part_index.get(id).copied()
|
||||
}
|
||||
|
||||
/// 按 Part 索引设置 opacity
|
||||
pub fn set_part_opacity_by_index(&self, core: &CubismCore, index: usize, opacity: f32) {
|
||||
if index < self.part_count {
|
||||
let slice = self.part_opacities_mut(core);
|
||||
slice[index] = opacity.clamp(0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// 按 Part 索引获取 opacity
|
||||
pub fn get_part_opacity_by_index(&self, core: &CubismCore, index: usize) -> Option<f32> {
|
||||
if index >= self.part_count {
|
||||
return None;
|
||||
}
|
||||
let slice = self.part_opacities_mut(core);
|
||||
Some(slice[index])
|
||||
}
|
||||
|
||||
/// 更新 model(应用参数变化,重新计算顶点/不透明度等)
|
||||
pub fn update(&self, core: &CubismCore) {
|
||||
unsafe { (core.csm_update_model)(self.model) };
|
||||
|
||||
233
src/plugins/live2d/pose.rs
Normal file
233
src/plugins/live2d/pose.rs
Normal file
@@ -0,0 +1,233 @@
|
||||
//! Pose 处理:根据 `.pose3.json` 管理互斥 Part 的显示状态。
|
||||
//!
|
||||
//! Haru 这类模型会把“手臂伸开”和“手臂抱胸”做成不同 Part。
|
||||
//! 如果不执行 Pose,多个 Part 会同时可见,画面上就会出现四只手臂。
|
||||
|
||||
use super::core_ffi::CubismCore;
|
||||
use super::model::CubismModel;
|
||||
use anyhow::{Context, Result};
|
||||
use serde::Deserialize;
|
||||
use std::path::Path;
|
||||
|
||||
const DEFAULT_FADE_IN_SECONDS: f32 = 0.5;
|
||||
const EPSILON: f32 = 0.001;
|
||||
const PHI: f32 = 0.5;
|
||||
const BACK_OPACITY_THRESHOLD: f32 = 0.15;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct PoseJson {
|
||||
#[serde(rename = "FadeInTime")]
|
||||
fade_in_time: Option<f32>,
|
||||
#[serde(rename = "Groups", default)]
|
||||
groups: Vec<Vec<PosePartJson>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct PosePartJson {
|
||||
#[serde(rename = "Id")]
|
||||
id: String,
|
||||
#[serde(rename = "Link", default)]
|
||||
link: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct PosePart {
|
||||
part_index: Option<usize>,
|
||||
param_index: Option<usize>,
|
||||
link_part_indices: Vec<usize>,
|
||||
virtual_param_value: f32,
|
||||
}
|
||||
|
||||
/// Live2D Pose 状态。
|
||||
pub struct PoseState {
|
||||
fade_time_seconds: f32,
|
||||
groups: Vec<Vec<PosePart>>,
|
||||
initialized: bool,
|
||||
}
|
||||
|
||||
impl PoseState {
|
||||
pub fn from_path(path: &Path, model: &CubismModel) -> Result<Self> {
|
||||
let text = std::fs::read_to_string(path)
|
||||
.with_context(|| format!("读取 pose3.json 失败: {}", path.display()))?;
|
||||
let json: PoseJson = serde_json::from_str(&text)
|
||||
.with_context(|| format!("解析 pose3.json 失败: {}", path.display()))?;
|
||||
|
||||
let fade_time_seconds = json
|
||||
.fade_in_time
|
||||
.filter(|v| *v >= 0.0)
|
||||
.unwrap_or(DEFAULT_FADE_IN_SECONDS);
|
||||
|
||||
let groups: Vec<Vec<PosePart>> = json
|
||||
.groups
|
||||
.into_iter()
|
||||
.map(|group| {
|
||||
group
|
||||
.into_iter()
|
||||
.map(|part| PosePart {
|
||||
param_index: model.param_index.get(&part.id).copied(),
|
||||
part_index: model.part_index(&part.id),
|
||||
link_part_indices: part
|
||||
.link
|
||||
.iter()
|
||||
.filter_map(|id| model.part_index(id))
|
||||
.collect(),
|
||||
virtual_param_value: 0.0,
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
|
||||
println!(
|
||||
"[Live2D] Pose 加载: {} groups={} fade={:.3}s",
|
||||
path.display(),
|
||||
groups.len(),
|
||||
fade_time_seconds
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
fade_time_seconds,
|
||||
groups,
|
||||
initialized: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// 初始化 Pose:每个互斥组保留当前最可见的 Part,隐藏其余 Part。
|
||||
pub fn reset(&mut self, core: &CubismCore, model: &CubismModel) {
|
||||
for group in &mut self.groups {
|
||||
let mut visible_index = 0;
|
||||
let mut max_opacity = -1.0;
|
||||
for (index, part) in group.iter().enumerate() {
|
||||
let opacity = part
|
||||
.part_index
|
||||
.and_then(|idx| model.get_part_opacity_by_index(core, idx))
|
||||
.unwrap_or(0.0);
|
||||
if opacity >= max_opacity {
|
||||
max_opacity = opacity;
|
||||
visible_index = index;
|
||||
}
|
||||
}
|
||||
|
||||
for (index, part) in group.iter_mut().enumerate() {
|
||||
let opacity = if index == visible_index { 1.0 } else { 0.0 };
|
||||
part.virtual_param_value = opacity;
|
||||
if let Some(param_index) = part.param_index {
|
||||
model.set_param_by_index(core, param_index, opacity);
|
||||
}
|
||||
if let Some(part_index) = part.part_index {
|
||||
model.set_part_opacity_by_index(core, part_index, opacity);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.copy_part_opacities(core, model);
|
||||
self.initialized = true;
|
||||
}
|
||||
|
||||
/// 按官方 CubismPose 的互斥淡入规则更新 PartOpacity。
|
||||
pub fn update_parameters(&mut self, core: &CubismCore, model: &CubismModel, dt: f32) {
|
||||
if !self.initialized {
|
||||
self.reset(core, model);
|
||||
}
|
||||
|
||||
let dt = dt.max(0.0);
|
||||
let fade_time_seconds = self.fade_time_seconds;
|
||||
for group in &mut self.groups {
|
||||
Self::do_fade(core, model, fade_time_seconds, dt, group);
|
||||
}
|
||||
self.copy_part_opacities(core, model);
|
||||
}
|
||||
|
||||
fn do_fade(
|
||||
core: &CubismCore,
|
||||
model: &CubismModel,
|
||||
fade_time_seconds: f32,
|
||||
dt: f32,
|
||||
group: &mut [PosePart],
|
||||
) {
|
||||
if group.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut visible_part_index = None;
|
||||
let mut new_opacity = 1.0;
|
||||
|
||||
for (index, part) in group.iter().enumerate() {
|
||||
if part.pose_value(core, model) <= EPSILON {
|
||||
continue;
|
||||
}
|
||||
|
||||
visible_part_index = Some(index);
|
||||
if fade_time_seconds == 0.0 {
|
||||
new_opacity = 1.0;
|
||||
} else {
|
||||
new_opacity = part
|
||||
.part_index
|
||||
.and_then(|idx| model.get_part_opacity_by_index(core, idx))
|
||||
.unwrap_or(0.0);
|
||||
new_opacity = (new_opacity + dt / fade_time_seconds).min(1.0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
let visible_part_index = visible_part_index.unwrap_or(0);
|
||||
if visible_part_index == 0 && group[0].pose_value(core, model) <= EPSILON {
|
||||
group[0].virtual_param_value = 1.0;
|
||||
if let Some(param_index) = group[0].param_index {
|
||||
model.set_param_by_index(core, param_index, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
for (index, part) in group.iter().enumerate() {
|
||||
let Some(part_index) = part.part_index else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if index == visible_part_index {
|
||||
model.set_part_opacity_by_index(core, part_index, new_opacity);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut opacity = model
|
||||
.get_part_opacity_by_index(core, part_index)
|
||||
.unwrap_or(0.0);
|
||||
let mut target_opacity = if new_opacity < PHI {
|
||||
new_opacity * (PHI - 1.0) / PHI + 1.0
|
||||
} else {
|
||||
(1.0 - new_opacity) * PHI / (1.0 - PHI)
|
||||
};
|
||||
|
||||
let back_opacity = (1.0 - target_opacity) * (1.0 - new_opacity);
|
||||
if back_opacity > BACK_OPACITY_THRESHOLD {
|
||||
target_opacity = 1.0 - BACK_OPACITY_THRESHOLD / (1.0 - new_opacity);
|
||||
}
|
||||
|
||||
if opacity > target_opacity {
|
||||
opacity = target_opacity;
|
||||
}
|
||||
model.set_part_opacity_by_index(core, part_index, opacity);
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_part_opacities(&self, core: &CubismCore, model: &CubismModel) {
|
||||
for group in &self.groups {
|
||||
for part in group {
|
||||
let Some(part_index) = part.part_index else {
|
||||
continue;
|
||||
};
|
||||
let Some(opacity) = model.get_part_opacity_by_index(core, part_index) else {
|
||||
continue;
|
||||
};
|
||||
for &link_part_index in &part.link_part_indices {
|
||||
model.set_part_opacity_by_index(core, link_part_index, opacity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PosePart {
|
||||
fn pose_value(&self, core: &CubismCore, model: &CubismModel) -> f32 {
|
||||
self.param_index
|
||||
.and_then(|idx| model.get_param_by_index(core, idx))
|
||||
.unwrap_or(self.virtual_param_value)
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ 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 super::pose::PoseState;
|
||||
use anyhow::Result;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
@@ -19,6 +20,7 @@ pub struct Live2DRenderer {
|
||||
pub gl_ctx: RenderContext,
|
||||
pub texture_ids: Vec<u32>,
|
||||
pub animation: AnimationState,
|
||||
pub pose: Option<PoseState>,
|
||||
pub model_matrix: [f32; 16],
|
||||
pub running: bool,
|
||||
}
|
||||
@@ -56,6 +58,15 @@ impl Live2DRenderer {
|
||||
let model_matrix = compute_model_matrix(canvas, gl_ctx.width, gl_ctx.height);
|
||||
|
||||
model.reset_to_default(&core);
|
||||
let mut pose = if let Some(pose_rel) = model3.file_references.pose.as_deref() {
|
||||
let pose_path = paths.model3_dir.join(pose_rel);
|
||||
Some(PoseState::from_path(&pose_path, &model)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(pose) = pose.as_mut() {
|
||||
pose.reset(&core, &model);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
core,
|
||||
@@ -65,6 +76,7 @@ impl Live2DRenderer {
|
||||
gl_ctx,
|
||||
texture_ids,
|
||||
animation: AnimationState::default(),
|
||||
pose,
|
||||
model_matrix,
|
||||
running: false,
|
||||
})
|
||||
@@ -79,6 +91,9 @@ impl Live2DRenderer {
|
||||
let dt = 1.0 / 60.0;
|
||||
|
||||
self.animation.update(&self.core, &self.model, now, dt);
|
||||
if let Some(pose) = self.pose.as_mut() {
|
||||
pose.update_parameters(&self.core, &self.model, dt);
|
||||
}
|
||||
self.model.update(&self.core);
|
||||
|
||||
let snapshots = self.model.drawable_snapshots(&self.core);
|
||||
@@ -426,4 +441,4 @@ fn compute_model_matrix(
|
||||
0.0, 0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 0.0, 1.0,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user