1#[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#[allow(dead_code)]
16#[derive(Clone, Debug)]
17pub struct SkinShaderParams {
18 pub sss_strength: f32,
20 pub roughness: f32,
22 pub melanin: f32,
24 pub hemoglobin: f32,
26 pub tint: [f32; 3],
28}
29
30#[allow(dead_code)]
32#[derive(Clone, Debug)]
33pub struct SkinPreset {
34 pub name: String,
36 pub zones: Vec<(SkinZone, SkinShaderParams)>,
38}
39
40#[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#[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#[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#[allow(dead_code)]
88pub fn set_roughness(params: &mut SkinShaderParams, roughness: f32) {
89 params.roughness = roughness.clamp(0.0, 1.0);
90}
91
92#[allow(dead_code)]
94pub fn set_melanin(params: &mut SkinShaderParams, melanin: f32) {
95 params.melanin = melanin.clamp(0.0, 1.0);
96}
97
98#[allow(dead_code)]
100pub fn set_hemoglobin(params: &mut SkinShaderParams, hemoglobin: f32) {
101 params.hemoglobin = hemoglobin.clamp(0.0, 1.0);
102}
103
104#[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#[allow(dead_code)]
134pub fn skin_color_from_params(params: &SkinShaderParams) -> [f32; 3] {
135 let base_r = 1.0;
137 let base_g = 0.85;
138 let base_b = 0.72;
139
140 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 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 [
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#[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#[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#[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#[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#[allow(dead_code)]
221pub fn preset_count(preset: &SkinPreset) -> usize {
222 preset.zones.len()
223}
224
225#[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); 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); }
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}