1struct Lcg(u64);
8
9impl Lcg {
10 fn new(seed: u64) -> Self {
11 Self(seed.wrapping_add(1))
12 }
13
14 fn next_u64(&mut self) -> u64 {
15 self.0 = self
16 .0
17 .wrapping_mul(6364136223846793005)
18 .wrapping_add(1442695040888963407);
19 self.0
20 }
21
22 fn next_f32(&mut self) -> f32 {
24 (self.next_u64() >> 11) as f32 / (1u64 << 53) as f32
25 }
26
27 fn range_f32(&mut self, lo: f32, hi: f32) -> f32 {
29 lo + self.next_f32() * (hi - lo)
30 }
31}
32
33#[allow(dead_code)]
34pub struct HairRegion {
35 pub name: String,
36 pub density: f32,
37 pub length: f32,
38 pub length_variance: f32,
39 pub curl: f32,
40 pub color: [f32; 3],
41 pub enabled: bool,
42}
43
44#[allow(dead_code)]
45pub struct HairProfile {
46 pub regions: Vec<HairRegion>,
47 pub global_density_scale: f32,
48 pub global_length_scale: f32,
49}
50
51#[allow(dead_code)]
52pub struct HairStrand {
53 pub root: [f32; 3],
54 pub tip: [f32; 3],
55 pub thickness: f32,
56 pub color: [f32; 3],
57}
58
59#[allow(dead_code)]
60pub struct HairGenerationParams {
61 pub seed: u64,
62 pub lod: u8,
63}
64
65#[allow(dead_code)]
66pub fn default_hair_profile() -> HairProfile {
67 HairProfile {
68 regions: vec![
69 HairRegion {
70 name: "scalp".to_string(),
71 density: 150.0,
72 length: 120.0,
73 length_variance: 30.0,
74 curl: 0.1,
75 color: [0.2, 0.15, 0.1],
76 enabled: true,
77 },
78 HairRegion {
79 name: "eyebrow_left".to_string(),
80 density: 80.0,
81 length: 10.0,
82 length_variance: 2.0,
83 curl: 0.05,
84 color: [0.18, 0.12, 0.08],
85 enabled: true,
86 },
87 HairRegion {
88 name: "eyebrow_right".to_string(),
89 density: 80.0,
90 length: 10.0,
91 length_variance: 2.0,
92 curl: 0.05,
93 color: [0.18, 0.12, 0.08],
94 enabled: true,
95 },
96 HairRegion {
97 name: "eyelash_upper".to_string(),
98 density: 100.0,
99 length: 12.0,
100 length_variance: 2.5,
101 curl: 0.3,
102 color: [0.1, 0.08, 0.05],
103 enabled: true,
104 },
105 HairRegion {
106 name: "eyelash_lower".to_string(),
107 density: 60.0,
108 length: 8.0,
109 length_variance: 1.5,
110 curl: 0.2,
111 color: [0.1, 0.08, 0.05],
112 enabled: true,
113 },
114 HairRegion {
115 name: "beard".to_string(),
116 density: 40.0,
117 length: 5.0,
118 length_variance: 2.0,
119 curl: 0.1,
120 color: [0.2, 0.15, 0.1],
121 enabled: false,
122 },
123 HairRegion {
124 name: "armpit".to_string(),
125 density: 20.0,
126 length: 25.0,
127 length_variance: 5.0,
128 curl: 0.2,
129 color: [0.22, 0.17, 0.12],
130 enabled: true,
131 },
132 HairRegion {
133 name: "pubic".to_string(),
134 density: 30.0,
135 length: 20.0,
136 length_variance: 5.0,
137 curl: 0.5,
138 color: [0.2, 0.15, 0.1],
139 enabled: true,
140 },
141 ],
142 global_density_scale: 1.0,
143 global_length_scale: 1.0,
144 }
145}
146
147#[allow(dead_code)]
148pub fn add_region(profile: &mut HairProfile, region: HairRegion) {
149 profile.regions.push(region);
150}
151
152#[allow(dead_code)]
153pub fn scale_density(profile: &mut HairProfile, factor: f32) {
154 profile.global_density_scale *= factor;
155}
156
157#[allow(dead_code)]
158pub fn hair_count_for_region(region: &HairRegion, area_cm2: f32) -> usize {
159 if !region.enabled {
160 return 0;
161 }
162 (region.density * area_cm2).round() as usize
163}
164
165#[allow(dead_code)]
166pub fn generate_strands(
167 region: &HairRegion,
168 positions: &[[f32; 3]],
169 normals: &[[f32; 3]],
170 params: &HairGenerationParams,
171) -> Vec<HairStrand> {
172 if !region.enabled || positions.is_empty() {
173 return Vec::new();
174 }
175 let lod_factor = lod_density_factor(params.lod);
176 let count = (positions.len() as f32 * lod_factor).round() as usize;
177 let count = count.min(positions.len());
178
179 let mut rng = Lcg::new(params.seed);
180 let mut strands = Vec::with_capacity(count);
181 for (i, root) in positions.iter().enumerate().take(count) {
182 let normal = if i < normals.len() {
183 normals[i]
184 } else {
185 [0.0, 1.0, 0.0]
186 };
187 let length_var = rng.range_f32(-region.length_variance, region.length_variance);
188 let length = (region.length + length_var).max(0.1) * 0.001; let tip = curl_tip(
190 *root,
191 normal,
192 length,
193 region.curl,
194 params.seed.wrapping_add(i as u64),
195 );
196 let thickness = rng.range_f32(0.04, 0.08);
197 strands.push(HairStrand {
198 root: *root,
199 tip,
200 thickness,
201 color: region.color,
202 });
203 }
204 strands
205}
206
207#[allow(dead_code)]
208pub fn curl_tip(root: [f32; 3], normal: [f32; 3], length: f32, curl: f32, seed: u64) -> [f32; 3] {
209 let s = seed
210 .wrapping_mul(6364136223846793005)
211 .wrapping_add(1442695040888963407);
212 let angle = (s as f32 / u64::MAX as f32) * std::f32::consts::TAU;
213 let curl_offset = curl * length * 0.5;
214 [
215 root[0] + normal[0] * length + angle.cos() * curl_offset,
216 root[1] + normal[1] * length + angle.sin() * curl_offset,
217 root[2] + normal[2] * length,
218 ]
219}
220
221#[allow(dead_code)]
222pub fn total_strand_count(profile: &HairProfile, area_cm2: f32) -> usize {
223 profile
224 .regions
225 .iter()
226 .filter(|r| r.enabled)
227 .map(|r| {
228 let effective_density = r.density * profile.global_density_scale;
229 (effective_density * area_cm2).round() as usize
230 })
231 .sum()
232}
233
234#[allow(dead_code)]
235pub fn region_by_name<'a>(profile: &'a HairProfile, name: &str) -> Option<&'a HairRegion> {
236 profile.regions.iter().find(|r| r.name == name)
237}
238
239#[allow(dead_code)]
240pub fn blend_hair_profiles(a: &HairProfile, b: &HairProfile, t: f32) -> HairProfile {
241 let t = t.clamp(0.0, 1.0);
242 let count = a.regions.len().min(b.regions.len());
243 let mut regions = Vec::with_capacity(count);
244 for i in 0..count {
245 let ra = &a.regions[i];
246 let rb = &b.regions[i];
247 regions.push(HairRegion {
248 name: ra.name.clone(),
249 density: ra.density * (1.0 - t) + rb.density * t,
250 length: ra.length * (1.0 - t) + rb.length * t,
251 length_variance: ra.length_variance * (1.0 - t) + rb.length_variance * t,
252 curl: ra.curl * (1.0 - t) + rb.curl * t,
253 color: [
254 ra.color[0] * (1.0 - t) + rb.color[0] * t,
255 ra.color[1] * (1.0 - t) + rb.color[1] * t,
256 ra.color[2] * (1.0 - t) + rb.color[2] * t,
257 ],
258 enabled: if t < 0.5 { ra.enabled } else { rb.enabled },
259 });
260 }
261 HairProfile {
262 regions,
263 global_density_scale: a.global_density_scale * (1.0 - t) + b.global_density_scale * t,
264 global_length_scale: a.global_length_scale * (1.0 - t) + b.global_length_scale * t,
265 }
266}
267
268#[allow(dead_code)]
269pub fn hair_profile_to_params(profile: &HairProfile) -> Vec<(String, f32)> {
270 let mut out = Vec::new();
271 out.push((
272 "global_density_scale".to_string(),
273 profile.global_density_scale,
274 ));
275 out.push((
276 "global_length_scale".to_string(),
277 profile.global_length_scale,
278 ));
279 for r in &profile.regions {
280 let prefix = r.name.clone();
281 out.push((format!("{prefix}.density"), r.density));
282 out.push((format!("{prefix}.length"), r.length));
283 out.push((format!("{prefix}.length_variance"), r.length_variance));
284 out.push((format!("{prefix}.curl"), r.curl));
285 out.push((format!("{prefix}.color_r"), r.color[0]));
286 out.push((format!("{prefix}.color_g"), r.color[1]));
287 out.push((format!("{prefix}.color_b"), r.color[2]));
288 out.push((
289 format!("{prefix}.enabled"),
290 if r.enabled { 1.0 } else { 0.0 },
291 ));
292 }
293 out
294}
295
296#[allow(dead_code)]
297pub fn lod_density_factor(lod: u8) -> f32 {
298 match lod {
299 0 => 1.0,
300 1 => 0.4,
301 _ => 0.1,
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 #[test]
310 fn test_default_profile_non_empty() {
311 let p = default_hair_profile();
312 assert!(!p.regions.is_empty());
313 assert!(p.regions.len() >= 8);
314 }
315
316 #[test]
317 fn test_default_profile_names() {
318 let p = default_hair_profile();
319 let names: Vec<&str> = p.regions.iter().map(|r| r.name.as_str()).collect();
320 assert!(names.contains(&"scalp"));
321 assert!(names.contains(&"beard"));
322 }
323
324 #[test]
325 fn test_hair_count_formula() {
326 let region = HairRegion {
327 name: "test".to_string(),
328 density: 100.0,
329 length: 10.0,
330 length_variance: 1.0,
331 curl: 0.0,
332 color: [0.0; 3],
333 enabled: true,
334 };
335 let count = hair_count_for_region(®ion, 5.0);
336 assert_eq!(count, 500);
337 }
338
339 #[test]
340 fn test_hair_count_disabled() {
341 let region = HairRegion {
342 name: "test".to_string(),
343 density: 100.0,
344 length: 10.0,
345 length_variance: 1.0,
346 curl: 0.0,
347 color: [0.0; 3],
348 enabled: false,
349 };
350 assert_eq!(hair_count_for_region(®ion, 10.0), 0);
351 }
352
353 #[test]
354 fn test_generate_strands_length() {
355 let region = HairRegion {
356 name: "test".to_string(),
357 density: 10.0,
358 length: 20.0,
359 length_variance: 2.0,
360 curl: 0.0,
361 color: [0.5, 0.4, 0.3],
362 enabled: true,
363 };
364 let positions: Vec<[f32; 3]> = (0..10).map(|i| [i as f32, 0.0, 0.0]).collect();
365 let normals: Vec<[f32; 3]> = (0..10).map(|_| [0.0, 1.0, 0.0]).collect();
366 let params = HairGenerationParams { seed: 42, lod: 0 };
367 let strands = generate_strands(®ion, &positions, &normals, ¶ms);
368 assert_eq!(strands.len(), 10);
369 }
370
371 #[test]
372 fn test_generate_strands_lod1() {
373 let region = HairRegion {
374 name: "test".to_string(),
375 density: 10.0,
376 length: 20.0,
377 length_variance: 2.0,
378 curl: 0.0,
379 color: [0.5, 0.4, 0.3],
380 enabled: true,
381 };
382 let positions: Vec<[f32; 3]> = (0..100).map(|i| [i as f32, 0.0, 0.0]).collect();
383 let normals: Vec<[f32; 3]> = (0..100).map(|_| [0.0, 1.0, 0.0]).collect();
384 let params = HairGenerationParams { seed: 42, lod: 1 };
385 let strands = generate_strands(®ion, &positions, &normals, ¶ms);
386 assert!(strands.len() < 100);
387 }
388
389 #[test]
390 fn test_generate_strands_disabled() {
391 let region = HairRegion {
392 name: "test".to_string(),
393 density: 10.0,
394 length: 20.0,
395 length_variance: 2.0,
396 curl: 0.0,
397 color: [0.5, 0.4, 0.3],
398 enabled: false,
399 };
400 let positions = vec![[0.0_f32; 3]];
401 let normals = vec![[0.0, 1.0, 0.0_f32]];
402 let params = HairGenerationParams { seed: 1, lod: 0 };
403 assert!(generate_strands(®ion, &positions, &normals, ¶ms).is_empty());
404 }
405
406 #[test]
407 fn test_blend_profiles() {
408 let a = default_hair_profile();
409 let b = default_hair_profile();
410 let blended = blend_hair_profiles(&a, &b, 0.5);
411 assert!(!blended.regions.is_empty());
412 assert!((blended.global_density_scale - 1.0).abs() < 1e-5);
413 }
414
415 #[test]
416 fn test_blend_profiles_t0() {
417 let a = default_hair_profile();
418 let mut b = default_hair_profile();
419 b.global_density_scale = 2.0;
420 let blended = blend_hair_profiles(&a, &b, 0.0);
421 assert!((blended.global_density_scale - 1.0).abs() < 1e-5);
422 }
423
424 #[test]
425 fn test_blend_profiles_t1() {
426 let a = default_hair_profile();
427 let mut b = default_hair_profile();
428 b.global_density_scale = 2.0;
429 let blended = blend_hair_profiles(&a, &b, 1.0);
430 assert!((blended.global_density_scale - 2.0).abs() < 1e-5);
431 }
432
433 #[test]
434 fn test_region_lookup() {
435 let p = default_hair_profile();
436 let r = region_by_name(&p, "scalp");
437 assert!(r.is_some());
438 assert_eq!(r.expect("should succeed").name, "scalp");
439 }
440
441 #[test]
442 fn test_region_lookup_missing() {
443 let p = default_hair_profile();
444 assert!(region_by_name(&p, "nonexistent").is_none());
445 }
446
447 #[test]
448 fn test_scale_density() {
449 let mut p = default_hair_profile();
450 scale_density(&mut p, 2.0);
451 assert!((p.global_density_scale - 2.0).abs() < 1e-5);
452 }
453
454 #[test]
455 fn test_lod_factor() {
456 assert!((lod_density_factor(0) - 1.0).abs() < 1e-5);
457 assert!((lod_density_factor(1) - 0.4).abs() < 1e-5);
458 assert!((lod_density_factor(2) - 0.1).abs() < 1e-5);
459 assert!((lod_density_factor(255) - 0.1).abs() < 1e-5);
460 }
461
462 #[test]
463 fn test_total_strand_count() {
464 let p = default_hair_profile();
465 let count = total_strand_count(&p, 1.0);
466 assert!(count > 0);
467 }
468
469 #[test]
470 fn test_hair_profile_to_params() {
471 let p = default_hair_profile();
472 let params = hair_profile_to_params(&p);
473 assert!(!params.is_empty());
474 let has_global = params.iter().any(|(k, _)| k == "global_density_scale");
475 assert!(has_global);
476 }
477
478 #[test]
479 fn test_curl_tip() {
480 let root = [0.0_f32; 3];
481 let normal = [0.0, 1.0, 0.0];
482 let tip = curl_tip(root, normal, 0.1, 0.0, 42);
483 assert!((tip[1] - 0.1).abs() < 0.01);
484 }
485
486 #[test]
487 fn test_add_region() {
488 let mut p = default_hair_profile();
489 let n = p.regions.len();
490 add_region(
491 &mut p,
492 HairRegion {
493 name: "extra".to_string(),
494 density: 5.0,
495 length: 5.0,
496 length_variance: 1.0,
497 curl: 0.0,
498 color: [1.0; 3],
499 enabled: true,
500 },
501 );
502 assert_eq!(p.regions.len(), n + 1);
503 }
504}