Skip to main content

oxihuman_morph/
skin_shader.rs

1//! Skin material/shader parameter morphs (SSS, roughness, color tints).
2
3/// Skin body zone classification.
4#[allow(dead_code)]
5#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
6pub enum SkinZone {
7    Face,
8    Neck,
9    Arms,
10    Torso,
11    Legs,
12}
13
14/// Parameters controlling skin material appearance.
15#[allow(dead_code)]
16#[derive(Clone, Debug)]
17pub struct SkinShaderParams {
18    /// Subsurface scattering strength (0..1).
19    pub sss_strength: f32,
20    /// Surface roughness (0 = mirror, 1 = fully rough).
21    pub roughness: f32,
22    /// Melanin level controlling skin darkness (0..1).
23    pub melanin: f32,
24    /// Hemoglobin level controlling redness (0..1).
25    pub hemoglobin: f32,
26    /// RGB tint applied on top of computed color.
27    pub tint: [f32; 3],
28}
29
30/// A named collection of skin shader parameters for all zones.
31#[allow(dead_code)]
32#[derive(Clone, Debug)]
33pub struct SkinPreset {
34    /// Human-readable preset name.
35    pub name: String,
36    /// Parameters for each of the 5 zones (Face, Neck, Arms, Torso, Legs).
37    pub zones: Vec<(SkinZone, SkinShaderParams)>,
38}
39
40// ---------------------------------------------------------------------------
41// Construction
42// ---------------------------------------------------------------------------
43
44/// Create default skin shader parameters (medium Caucasian skin).
45#[allow(dead_code)]
46pub fn default_skin_params() -> SkinShaderParams {
47    SkinShaderParams {
48        sss_strength: 0.5,
49        roughness: 0.4,
50        melanin: 0.3,
51        hemoglobin: 0.2,
52        tint: [1.0, 1.0, 1.0],
53    }
54}
55
56/// Create a new skin preset with the given name and default params for all zones.
57#[allow(dead_code)]
58pub fn new_skin_preset(name: &str) -> SkinPreset {
59    let all_zones = [
60        SkinZone::Face,
61        SkinZone::Neck,
62        SkinZone::Arms,
63        SkinZone::Torso,
64        SkinZone::Legs,
65    ];
66    let zones = all_zones
67        .iter()
68        .map(|&z| (z, default_skin_params()))
69        .collect();
70    SkinPreset {
71        name: name.to_string(),
72        zones,
73    }
74}
75
76// ---------------------------------------------------------------------------
77// Setters
78// ---------------------------------------------------------------------------
79
80/// Set subsurface scattering strength, clamped to [0, 1].
81#[allow(dead_code)]
82pub fn set_sss_strength(params: &mut SkinShaderParams, strength: f32) {
83    params.sss_strength = strength.clamp(0.0, 1.0);
84}
85
86/// Set surface roughness, clamped to [0, 1].
87#[allow(dead_code)]
88pub fn set_roughness(params: &mut SkinShaderParams, roughness: f32) {
89    params.roughness = roughness.clamp(0.0, 1.0);
90}
91
92/// Set melanin level (skin darkness), clamped to [0, 1].
93#[allow(dead_code)]
94pub fn set_melanin(params: &mut SkinShaderParams, melanin: f32) {
95    params.melanin = melanin.clamp(0.0, 1.0);
96}
97
98/// Set hemoglobin level (redness), clamped to [0, 1].
99#[allow(dead_code)]
100pub fn set_hemoglobin(params: &mut SkinShaderParams, hemoglobin: f32) {
101    params.hemoglobin = hemoglobin.clamp(0.0, 1.0);
102}
103
104// ---------------------------------------------------------------------------
105// Operations
106// ---------------------------------------------------------------------------
107
108/// Blend two skin parameter sets by factor `t` (0 = all `a`, 1 = all `b`).
109#[allow(dead_code)]
110pub fn blend_skin_params(a: &SkinShaderParams, b: &SkinShaderParams, t: f32) -> SkinShaderParams {
111    let t = t.clamp(0.0, 1.0);
112    let inv = 1.0 - t;
113    SkinShaderParams {
114        sss_strength: a.sss_strength * inv + b.sss_strength * t,
115        roughness: a.roughness * inv + b.roughness * t,
116        melanin: a.melanin * inv + b.melanin * t,
117        hemoglobin: a.hemoglobin * inv + b.hemoglobin * t,
118        tint: [
119            a.tint[0] * inv + b.tint[0] * t,
120            a.tint[1] * inv + b.tint[1] * t,
121            a.tint[2] * inv + b.tint[2] * t,
122        ],
123    }
124}
125
126/// Compute an approximate RGB skin color from melanin and hemoglobin values.
127///
128/// This uses a simplified model:
129/// - Base color starts light and darkens with melanin.
130/// - Red channel is boosted by hemoglobin.
131///
132/// Returns `[r, g, b]` each in 0..1.
133#[allow(dead_code)]
134pub fn skin_color_from_params(params: &SkinShaderParams) -> [f32; 3] {
135    // Base skin color (light skin).
136    let base_r = 1.0;
137    let base_g = 0.85;
138    let base_b = 0.72;
139
140    // Melanin darkens all channels.
141    let mel = params.melanin;
142    let r = base_r * (1.0 - mel * 0.7);
143    let g = base_g * (1.0 - mel * 0.75);
144    let b = base_b * (1.0 - mel * 0.8);
145
146    // Hemoglobin adds redness.
147    let hemo = params.hemoglobin;
148    let r = (r + hemo * 0.15).min(1.0);
149    let g = (g - hemo * 0.05).max(0.0);
150    let b = (b - hemo * 0.08).max(0.0);
151
152    // Apply tint.
153    [
154        (r * params.tint[0]).clamp(0.0, 1.0),
155        (g * params.tint[1]).clamp(0.0, 1.0),
156        (b * params.tint[2]).clamp(0.0, 1.0),
157    ]
158}
159
160/// Apply aging effects: increases roughness, reduces SSS, slightly increases melanin.
161#[allow(dead_code)]
162pub fn apply_age_effect(params: &mut SkinShaderParams, age_factor: f32) {
163    let factor = age_factor.clamp(0.0, 1.0);
164    params.roughness = (params.roughness + factor * 0.3).min(1.0);
165    params.sss_strength = (params.sss_strength - factor * 0.2).max(0.0);
166    params.melanin = (params.melanin + factor * 0.05).min(1.0);
167}
168
169/// Get the parameters for a specific zone from a preset.
170/// Returns `None` if the zone is not in the preset.
171#[allow(dead_code)]
172pub fn zone_params(preset: &SkinPreset, zone: SkinZone) -> Option<&SkinShaderParams> {
173    preset
174        .zones
175        .iter()
176        .find(|(z, _)| *z == zone)
177        .map(|(_, p)| p)
178}
179
180/// Set a color tint for a specific zone in the preset.
181/// Returns `true` if the zone was found and updated.
182#[allow(dead_code)]
183pub fn set_zone_tint(preset: &mut SkinPreset, zone: SkinZone, tint: [f32; 3]) -> bool {
184    for (z, p) in &mut preset.zones {
185        if *z == zone {
186            p.tint = tint;
187            return true;
188        }
189    }
190    false
191}
192
193/// Serialize a skin preset to a JSON string.
194#[allow(dead_code)]
195pub fn skin_preset_to_json(preset: &SkinPreset) -> String {
196    let mut parts = Vec::new();
197    for (zone, params) in &preset.zones {
198        let zone_name = match zone {
199            SkinZone::Face => "Face",
200            SkinZone::Neck => "Neck",
201            SkinZone::Arms => "Arms",
202            SkinZone::Torso => "Torso",
203            SkinZone::Legs => "Legs",
204        };
205        let color = skin_color_from_params(params);
206        parts.push(format!(
207            "{{\"zone\":\"{}\",\"sss\":{:.4},\"roughness\":{:.4},\"melanin\":{:.4},\"hemoglobin\":{:.4},\"color\":[{:.4},{:.4},{:.4}]}}",
208            zone_name, params.sss_strength, params.roughness, params.melanin, params.hemoglobin,
209            color[0], color[1], color[2]
210        ));
211    }
212    format!(
213        "{{\"name\":\"{}\",\"zones\":[{}]}}",
214        preset.name,
215        parts.join(",")
216    )
217}
218
219/// Return the number of zones in the preset.
220#[allow(dead_code)]
221pub fn preset_count(preset: &SkinPreset) -> usize {
222    preset.zones.len()
223}
224
225// ---------------------------------------------------------------------------
226// Tests
227// ---------------------------------------------------------------------------
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    #[test]
234    fn test_default_skin_params() {
235        let p = default_skin_params();
236        assert!(p.sss_strength > 0.0);
237        assert!(p.roughness > 0.0);
238        assert!(p.melanin >= 0.0 && p.melanin <= 1.0);
239    }
240
241    #[test]
242    fn test_new_skin_preset_has_all_zones() {
243        let preset = new_skin_preset("test");
244        assert_eq!(preset.zones.len(), 5);
245        assert_eq!(preset.name, "test");
246    }
247
248    #[test]
249    fn test_set_sss_strength() {
250        let mut p = default_skin_params();
251        set_sss_strength(&mut p, 0.8);
252        assert!((p.sss_strength - 0.8).abs() < 1e-6);
253    }
254
255    #[test]
256    fn test_set_sss_strength_clamps() {
257        let mut p = default_skin_params();
258        set_sss_strength(&mut p, 2.0);
259        assert!((p.sss_strength - 1.0).abs() < 1e-6);
260        set_sss_strength(&mut p, -1.0);
261        assert!((p.sss_strength - 0.0).abs() < 1e-6);
262    }
263
264    #[test]
265    fn test_set_roughness() {
266        let mut p = default_skin_params();
267        set_roughness(&mut p, 0.7);
268        assert!((p.roughness - 0.7).abs() < 1e-6);
269    }
270
271    #[test]
272    fn test_set_melanin() {
273        let mut p = default_skin_params();
274        set_melanin(&mut p, 0.9);
275        assert!((p.melanin - 0.9).abs() < 1e-6);
276    }
277
278    #[test]
279    fn test_set_hemoglobin() {
280        let mut p = default_skin_params();
281        set_hemoglobin(&mut p, 0.6);
282        assert!((p.hemoglobin - 0.6).abs() < 1e-6);
283    }
284
285    #[test]
286    fn test_blend_skin_params_zero() {
287        let a = default_skin_params();
288        let mut b = default_skin_params();
289        b.sss_strength = 1.0;
290        let r = blend_skin_params(&a, &b, 0.0);
291        assert!((r.sss_strength - a.sss_strength).abs() < 1e-6);
292    }
293
294    #[test]
295    fn test_blend_skin_params_one() {
296        let a = default_skin_params();
297        let mut b = default_skin_params();
298        b.sss_strength = 1.0;
299        let r = blend_skin_params(&a, &b, 1.0);
300        assert!((r.sss_strength - 1.0).abs() < 1e-6);
301    }
302
303    #[test]
304    fn test_skin_color_from_params_light() {
305        let p = SkinShaderParams {
306            sss_strength: 0.5,
307            roughness: 0.4,
308            melanin: 0.0,
309            hemoglobin: 0.0,
310            tint: [1.0, 1.0, 1.0],
311        };
312        let c = skin_color_from_params(&p);
313        assert!(c[0] > 0.9); // light skin = bright
314        assert!(c[1] > 0.8);
315    }
316
317    #[test]
318    fn test_skin_color_from_params_dark() {
319        let p = SkinShaderParams {
320            sss_strength: 0.5,
321            roughness: 0.4,
322            melanin: 1.0,
323            hemoglobin: 0.0,
324            tint: [1.0, 1.0, 1.0],
325        };
326        let c = skin_color_from_params(&p);
327        assert!(c[0] < 0.5); // dark skin = darker
328    }
329
330    #[test]
331    fn test_apply_age_effect() {
332        let mut p = default_skin_params();
333        let orig_roughness = p.roughness;
334        let orig_sss = p.sss_strength;
335        apply_age_effect(&mut p, 0.5);
336        assert!(p.roughness > orig_roughness);
337        assert!(p.sss_strength < orig_sss);
338    }
339
340    #[test]
341    fn test_zone_params_found() {
342        let preset = new_skin_preset("test");
343        let p = zone_params(&preset, SkinZone::Face);
344        assert!(p.is_some());
345    }
346
347    #[test]
348    fn test_set_zone_tint() {
349        let mut preset = new_skin_preset("test");
350        let ok = set_zone_tint(&mut preset, SkinZone::Arms, [0.5, 0.6, 0.7]);
351        assert!(ok);
352        let p = zone_params(&preset, SkinZone::Arms).expect("should succeed");
353        assert!((p.tint[0] - 0.5).abs() < 1e-6);
354    }
355
356    #[test]
357    fn test_skin_preset_to_json() {
358        let preset = new_skin_preset("demo");
359        let json = skin_preset_to_json(&preset);
360        assert!(json.contains("\"name\":\"demo\""));
361        assert!(json.contains("\"zone\":\"Face\""));
362    }
363
364    #[test]
365    fn test_preset_count() {
366        let preset = new_skin_preset("test");
367        assert_eq!(preset_count(&preset), 5);
368    }
369}