//! 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, #[serde(rename = "Groups", default)] groups: Vec>, } #[derive(Deserialize)] struct PosePartJson { #[serde(rename = "Id")] id: String, #[serde(rename = "Link", default)] link: Vec, } #[derive(Clone, Debug)] struct PosePart { part_index: Option, param_index: Option, link_part_indices: Vec, virtual_param_value: f32, } /// Live2D Pose 状态。 pub struct PoseState { fade_time_seconds: f32, groups: Vec>, initialized: bool, } impl PoseState { pub fn from_path(path: &Path, model: &CubismModel) -> Result { 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> = 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) } }