#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlendAccumMode {
Additive,
Overwrite,
Average,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct BlendShapeConfig {
pub normalize_weights: bool,
pub accumulation: BlendAccumMode,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct BlendTarget {
pub name: String,
pub deltas: Vec<[f32; 3]>,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct BlendShapeResult {
pub positions: Vec<[f32; 3]>,
pub active_targets: usize,
}
#[allow(dead_code)]
pub fn default_blend_shape_config() -> BlendShapeConfig {
BlendShapeConfig {
normalize_weights: false,
accumulation: BlendAccumMode::Additive,
}
}
#[allow(dead_code)]
pub fn new_blend_target(name: &str, deltas: Vec<[f32; 3]>) -> BlendTarget {
BlendTarget {
name: name.to_string(),
deltas,
}
}
#[allow(dead_code)]
pub fn evaluate_blend_shapes(
base: &[[f32; 3]],
targets: &[BlendTarget],
weights: &[f32],
cfg: &BlendShapeConfig,
) -> BlendShapeResult {
let n = base.len();
let mut positions: Vec<[f32; 3]> = base.to_vec();
let mut active_targets = 0usize;
let effective_weights: Vec<f32> = if cfg.normalize_weights {
normalize_blend_weights(weights)
} else {
weights.to_vec()
};
let n_targets = targets.len().min(effective_weights.len());
match cfg.accumulation {
BlendAccumMode::Additive => {
for (target, &w) in targets[..n_targets].iter().zip(effective_weights.iter()) {
if w == 0.0 {
continue;
}
active_targets += 1;
let m = n.min(target.deltas.len());
for (pos, &delta) in positions[..m].iter_mut().zip(target.deltas[..m].iter()) {
add_weighted_delta(pos, delta, w);
}
}
}
BlendAccumMode::Overwrite => {
if let Some((best_idx, _)) = effective_weights[..n_targets]
.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
{
let w = effective_weights[best_idx];
if w > 0.0 {
active_targets = 1;
let target = &targets[best_idx];
let m = n.min(target.deltas.len());
for (pos, &delta) in positions[..m].iter_mut().zip(target.deltas[..m].iter()) {
add_weighted_delta(pos, delta, w);
}
}
}
}
BlendAccumMode::Average => {
let weight_sum: f32 = effective_weights[..n_targets]
.iter()
.filter(|&&w| w > 0.0)
.sum();
if weight_sum > 1e-12 {
let mut acc: Vec<[f32; 3]> = vec![[0.0; 3]; n];
for (target, &w) in targets[..n_targets].iter().zip(effective_weights.iter()) {
if w <= 0.0 {
continue;
}
active_targets += 1;
let m = n.min(target.deltas.len());
for (a, &delta) in acc[..m].iter_mut().zip(target.deltas[..m].iter()) {
a[0] += delta[0] * w;
a[1] += delta[1] * w;
a[2] += delta[2] * w;
}
}
for (pos, a) in positions.iter_mut().zip(acc.iter()) {
pos[0] += a[0] / weight_sum;
pos[1] += a[1] / weight_sum;
pos[2] += a[2] / weight_sum;
}
}
}
}
BlendShapeResult {
positions,
active_targets,
}
}
#[allow(dead_code)]
#[inline]
pub fn blend_target_delta_count(t: &BlendTarget) -> usize {
t.deltas.len()
}
#[allow(dead_code)]
pub fn normalize_blend_weights(weights: &[f32]) -> Vec<f32> {
let s: f32 = weights.iter().sum();
if s <= 0.0 {
vec![0.0; weights.len()]
} else {
weights.iter().map(|&w| w / s).collect()
}
}
#[allow(dead_code)]
pub fn blend_shape_result_to_json(r: &BlendShapeResult) -> String {
format!(
"{{\"active_targets\":{},\"vertex_count\":{}}}",
r.active_targets,
r.positions.len()
)
}
#[allow(dead_code)]
#[inline]
pub fn add_weighted_delta(pos: &mut [f32; 3], delta: [f32; 3], weight: f32) {
pos[0] += delta[0] * weight;
pos[1] += delta[1] * weight;
pos[2] += delta[2] * weight;
}
#[allow(dead_code)]
pub fn blend_target_to_json(t: &BlendTarget) -> String {
format!(
"{{\"name\":\"{}\",\"delta_count\":{}}}",
t.name,
t.deltas.len()
)
}
#[allow(dead_code)]
pub fn max_displacement(r: &BlendShapeResult, base: &[[f32; 3]]) -> f32 {
r.positions
.iter()
.zip(base.iter())
.map(|(p, b)| {
let dx = p[0] - b[0];
let dy = p[1] - b[1];
let dz = p[2] - b[2];
(dx * dx + dy * dy + dz * dz).sqrt()
})
.fold(0.0_f32, f32::max)
}
#[allow(dead_code)]
pub fn accum_mode_name(cfg: &BlendShapeConfig) -> &'static str {
match cfg.accumulation {
BlendAccumMode::Additive => "additive",
BlendAccumMode::Overwrite => "overwrite",
BlendAccumMode::Average => "average",
}
}
#[cfg(test)]
mod tests {
use super::*;
fn simple_base() -> Vec<[f32; 3]> {
vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
}
fn smile_target() -> BlendTarget {
new_blend_target(
"smile",
vec![[0.0, 0.1, 0.0], [0.0, 0.1, 0.0], [0.0, 0.2, 0.0]],
)
}
#[test]
fn default_config_is_additive() {
let cfg = default_blend_shape_config();
assert_eq!(cfg.accumulation, BlendAccumMode::Additive);
assert!(!cfg.normalize_weights);
}
#[test]
fn additive_blend_at_full_weight() {
let base = simple_base();
let target = smile_target();
let cfg = BlendShapeConfig {
normalize_weights: false,
accumulation: BlendAccumMode::Additive,
};
let result = evaluate_blend_shapes(&base, &[target], &[1.0], &cfg);
assert!((result.positions[0][1] - 0.1).abs() < 1e-6);
assert_eq!(result.active_targets, 1);
}
#[test]
fn zero_weight_produces_no_change() {
let base = simple_base();
let target = smile_target();
let cfg = default_blend_shape_config();
let result = evaluate_blend_shapes(&base, &[target], &[0.0], &cfg);
for (p, b) in result.positions.iter().zip(base.iter()) {
assert!((p[0] - b[0]).abs() < 1e-6);
assert!((p[1] - b[1]).abs() < 1e-6);
}
assert_eq!(result.active_targets, 0);
}
#[test]
fn normalize_weights_sums_to_one() {
let weights = vec![2.0_f32, 2.0, 2.0];
let normalized = normalize_blend_weights(&weights);
let sum: f32 = normalized.iter().sum();
assert!((sum - 1.0).abs() < 1e-6);
}
#[test]
fn blend_target_to_json_has_name() {
let t = smile_target();
let json = blend_target_to_json(&t);
assert!(json.contains("smile"));
assert!(json.contains("delta_count"));
}
#[test]
fn max_displacement_full_weight() {
let base = simple_base();
let target = smile_target();
let cfg = default_blend_shape_config();
let result = evaluate_blend_shapes(&base, &[target], &[1.0], &cfg);
let disp = max_displacement(&result, &base);
assert!(disp > 0.0);
}
#[test]
fn accum_mode_name_strings() {
assert_eq!(accum_mode_name(&BlendShapeConfig { normalize_weights: false, accumulation: BlendAccumMode::Additive }), "additive");
assert_eq!(accum_mode_name(&BlendShapeConfig { normalize_weights: false, accumulation: BlendAccumMode::Overwrite }), "overwrite");
assert_eq!(accum_mode_name(&BlendShapeConfig { normalize_weights: false, accumulation: BlendAccumMode::Average }), "average");
}
#[test]
fn overwrite_mode_applies_best_weight() {
let base = simple_base();
let t1 = new_blend_target("low", vec![[0.0, 0.1, 0.0]; 3]);
let t2 = new_blend_target("high", vec![[0.0, 0.5, 0.0]; 3]);
let cfg = BlendShapeConfig {
normalize_weights: false,
accumulation: BlendAccumMode::Overwrite,
};
let result = evaluate_blend_shapes(&base, &[t1, t2], &[0.2, 0.8], &cfg);
assert!((result.positions[0][1] - 0.4).abs() < 1e-5);
}
}