234 lines
7.5 KiB
Rust
234 lines
7.5 KiB
Rust
//! 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)
|
||
}
|
||
}
|