#[allow(dead_code)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum SkinZone {
Face,
Neck,
Arms,
Torso,
Legs,
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct SkinShaderParams {
pub sss_strength: f32,
pub roughness: f32,
pub melanin: f32,
pub hemoglobin: f32,
pub tint: [f32; 3],
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct SkinPreset {
pub name: String,
pub zones: Vec<(SkinZone, SkinShaderParams)>,
}
#[allow(dead_code)]
pub fn default_skin_params() -> SkinShaderParams {
SkinShaderParams {
sss_strength: 0.5,
roughness: 0.4,
melanin: 0.3,
hemoglobin: 0.2,
tint: [1.0, 1.0, 1.0],
}
}
#[allow(dead_code)]
pub fn new_skin_preset(name: &str) -> SkinPreset {
let all_zones = [
SkinZone::Face,
SkinZone::Neck,
SkinZone::Arms,
SkinZone::Torso,
SkinZone::Legs,
];
let zones = all_zones
.iter()
.map(|&z| (z, default_skin_params()))
.collect();
SkinPreset {
name: name.to_string(),
zones,
}
}
#[allow(dead_code)]
pub fn set_sss_strength(params: &mut SkinShaderParams, strength: f32) {
params.sss_strength = strength.clamp(0.0, 1.0);
}
#[allow(dead_code)]
pub fn set_roughness(params: &mut SkinShaderParams, roughness: f32) {
params.roughness = roughness.clamp(0.0, 1.0);
}
#[allow(dead_code)]
pub fn set_melanin(params: &mut SkinShaderParams, melanin: f32) {
params.melanin = melanin.clamp(0.0, 1.0);
}
#[allow(dead_code)]
pub fn set_hemoglobin(params: &mut SkinShaderParams, hemoglobin: f32) {
params.hemoglobin = hemoglobin.clamp(0.0, 1.0);
}
#[allow(dead_code)]
pub fn blend_skin_params(a: &SkinShaderParams, b: &SkinShaderParams, t: f32) -> SkinShaderParams {
let t = t.clamp(0.0, 1.0);
let inv = 1.0 - t;
SkinShaderParams {
sss_strength: a.sss_strength * inv + b.sss_strength * t,
roughness: a.roughness * inv + b.roughness * t,
melanin: a.melanin * inv + b.melanin * t,
hemoglobin: a.hemoglobin * inv + b.hemoglobin * t,
tint: [
a.tint[0] * inv + b.tint[0] * t,
a.tint[1] * inv + b.tint[1] * t,
a.tint[2] * inv + b.tint[2] * t,
],
}
}
#[allow(dead_code)]
pub fn skin_color_from_params(params: &SkinShaderParams) -> [f32; 3] {
let base_r = 1.0;
let base_g = 0.85;
let base_b = 0.72;
let mel = params.melanin;
let r = base_r * (1.0 - mel * 0.7);
let g = base_g * (1.0 - mel * 0.75);
let b = base_b * (1.0 - mel * 0.8);
let hemo = params.hemoglobin;
let r = (r + hemo * 0.15).min(1.0);
let g = (g - hemo * 0.05).max(0.0);
let b = (b - hemo * 0.08).max(0.0);
[
(r * params.tint[0]).clamp(0.0, 1.0),
(g * params.tint[1]).clamp(0.0, 1.0),
(b * params.tint[2]).clamp(0.0, 1.0),
]
}
#[allow(dead_code)]
pub fn apply_age_effect(params: &mut SkinShaderParams, age_factor: f32) {
let factor = age_factor.clamp(0.0, 1.0);
params.roughness = (params.roughness + factor * 0.3).min(1.0);
params.sss_strength = (params.sss_strength - factor * 0.2).max(0.0);
params.melanin = (params.melanin + factor * 0.05).min(1.0);
}
#[allow(dead_code)]
pub fn zone_params(preset: &SkinPreset, zone: SkinZone) -> Option<&SkinShaderParams> {
preset
.zones
.iter()
.find(|(z, _)| *z == zone)
.map(|(_, p)| p)
}
#[allow(dead_code)]
pub fn set_zone_tint(preset: &mut SkinPreset, zone: SkinZone, tint: [f32; 3]) -> bool {
for (z, p) in &mut preset.zones {
if *z == zone {
p.tint = tint;
return true;
}
}
false
}
#[allow(dead_code)]
pub fn skin_preset_to_json(preset: &SkinPreset) -> String {
let mut parts = Vec::new();
for (zone, params) in &preset.zones {
let zone_name = match zone {
SkinZone::Face => "Face",
SkinZone::Neck => "Neck",
SkinZone::Arms => "Arms",
SkinZone::Torso => "Torso",
SkinZone::Legs => "Legs",
};
let color = skin_color_from_params(params);
parts.push(format!(
"{{\"zone\":\"{}\",\"sss\":{:.4},\"roughness\":{:.4},\"melanin\":{:.4},\"hemoglobin\":{:.4},\"color\":[{:.4},{:.4},{:.4}]}}",
zone_name, params.sss_strength, params.roughness, params.melanin, params.hemoglobin,
color[0], color[1], color[2]
));
}
format!(
"{{\"name\":\"{}\",\"zones\":[{}]}}",
preset.name,
parts.join(",")
)
}
#[allow(dead_code)]
pub fn preset_count(preset: &SkinPreset) -> usize {
preset.zones.len()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_skin_params() {
let p = default_skin_params();
assert!(p.sss_strength > 0.0);
assert!(p.roughness > 0.0);
assert!(p.melanin >= 0.0 && p.melanin <= 1.0);
}
#[test]
fn test_new_skin_preset_has_all_zones() {
let preset = new_skin_preset("test");
assert_eq!(preset.zones.len(), 5);
assert_eq!(preset.name, "test");
}
#[test]
fn test_set_sss_strength() {
let mut p = default_skin_params();
set_sss_strength(&mut p, 0.8);
assert!((p.sss_strength - 0.8).abs() < 1e-6);
}
#[test]
fn test_set_sss_strength_clamps() {
let mut p = default_skin_params();
set_sss_strength(&mut p, 2.0);
assert!((p.sss_strength - 1.0).abs() < 1e-6);
set_sss_strength(&mut p, -1.0);
assert!((p.sss_strength - 0.0).abs() < 1e-6);
}
#[test]
fn test_set_roughness() {
let mut p = default_skin_params();
set_roughness(&mut p, 0.7);
assert!((p.roughness - 0.7).abs() < 1e-6);
}
#[test]
fn test_set_melanin() {
let mut p = default_skin_params();
set_melanin(&mut p, 0.9);
assert!((p.melanin - 0.9).abs() < 1e-6);
}
#[test]
fn test_set_hemoglobin() {
let mut p = default_skin_params();
set_hemoglobin(&mut p, 0.6);
assert!((p.hemoglobin - 0.6).abs() < 1e-6);
}
#[test]
fn test_blend_skin_params_zero() {
let a = default_skin_params();
let mut b = default_skin_params();
b.sss_strength = 1.0;
let r = blend_skin_params(&a, &b, 0.0);
assert!((r.sss_strength - a.sss_strength).abs() < 1e-6);
}
#[test]
fn test_blend_skin_params_one() {
let a = default_skin_params();
let mut b = default_skin_params();
b.sss_strength = 1.0;
let r = blend_skin_params(&a, &b, 1.0);
assert!((r.sss_strength - 1.0).abs() < 1e-6);
}
#[test]
fn test_skin_color_from_params_light() {
let p = SkinShaderParams {
sss_strength: 0.5,
roughness: 0.4,
melanin: 0.0,
hemoglobin: 0.0,
tint: [1.0, 1.0, 1.0],
};
let c = skin_color_from_params(&p);
assert!(c[0] > 0.9); assert!(c[1] > 0.8);
}
#[test]
fn test_skin_color_from_params_dark() {
let p = SkinShaderParams {
sss_strength: 0.5,
roughness: 0.4,
melanin: 1.0,
hemoglobin: 0.0,
tint: [1.0, 1.0, 1.0],
};
let c = skin_color_from_params(&p);
assert!(c[0] < 0.5); }
#[test]
fn test_apply_age_effect() {
let mut p = default_skin_params();
let orig_roughness = p.roughness;
let orig_sss = p.sss_strength;
apply_age_effect(&mut p, 0.5);
assert!(p.roughness > orig_roughness);
assert!(p.sss_strength < orig_sss);
}
#[test]
fn test_zone_params_found() {
let preset = new_skin_preset("test");
let p = zone_params(&preset, SkinZone::Face);
assert!(p.is_some());
}
#[test]
fn test_set_zone_tint() {
let mut preset = new_skin_preset("test");
let ok = set_zone_tint(&mut preset, SkinZone::Arms, [0.5, 0.6, 0.7]);
assert!(ok);
let p = zone_params(&preset, SkinZone::Arms).expect("should succeed");
assert!((p.tint[0] - 0.5).abs() < 1e-6);
}
#[test]
fn test_skin_preset_to_json() {
let preset = new_skin_preset("demo");
let json = skin_preset_to_json(&preset);
assert!(json.contains("\"name\":\"demo\""));
assert!(json.contains("\"zone\":\"Face\""));
}
#[test]
fn test_preset_count() {
let preset = new_skin_preset("test");
assert_eq!(preset_count(&preset), 5);
}
}