Files
ShowenV2/src/plugins/live2d/pose.rs

234 lines
7.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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)
}
}