1#[allow(dead_code)]
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum HairProfile {
11 Straight,
12 Wavy,
13 Curly,
14 Coily,
15}
16
17#[allow(dead_code)]
19#[derive(Clone, Debug)]
20pub struct HairStrandConfig {
21 pub point_count: usize,
23 pub length: f32,
25 pub gravity: f32,
27 pub curl_freq: f32,
29 pub curl_amp: f32,
31}
32
33#[allow(dead_code)]
35#[derive(Clone, Debug)]
36pub struct HairStrand {
37 pub points: Vec<f32>,
39 pub profile: HairProfile,
41 pub rest_length: f32,
43}
44
45#[allow(dead_code)]
47pub type BoundingBox = ([f32; 3], [f32; 3]);
48
49struct Lcg(u64);
51
52impl Lcg {
53 fn new(seed: u64) -> Self {
54 Self(seed.wrapping_add(1))
55 }
56
57 fn next_u64(&mut self) -> u64 {
58 self.0 = self
59 .0
60 .wrapping_mul(6364136223846793005)
61 .wrapping_add(1442695040888963407);
62 self.0
63 }
64
65 fn next_f32(&mut self) -> f32 {
66 (self.next_u64() >> 11) as f32 / (1u64 << 53) as f32
67 }
68}
69
70#[allow(dead_code)]
76pub fn default_strand_config() -> HairStrandConfig {
77 HairStrandConfig {
78 point_count: 16,
79 length: 0.3,
80 gravity: 9.81,
81 curl_freq: 4.0,
82 curl_amp: 0.01,
83 }
84}
85
86#[allow(dead_code)]
88pub fn new_hair_strand(profile: HairProfile) -> HairStrand {
89 HairStrand {
90 points: Vec::new(),
91 profile,
92 rest_length: 0.0,
93 }
94}
95
96#[allow(dead_code)]
103pub fn generate_strand_points(
104 root: [f32; 3],
105 direction: [f32; 3],
106 config: &HairStrandConfig,
107 profile: HairProfile,
108) -> HairStrand {
109 let n = config.point_count.max(2);
110 let seg_len = config.length / (n - 1) as f32;
111
112 let dlen =
114 (direction[0] * direction[0] + direction[1] * direction[1] + direction[2] * direction[2])
115 .sqrt()
116 .max(1e-12);
117 let dir = [
118 direction[0] / dlen,
119 direction[1] / dlen,
120 direction[2] / dlen,
121 ];
122
123 let tangent = if dir[1].abs() < 0.9 {
125 let cx = dir[2];
127 let cy = 0.0;
128 let cz = -dir[0];
129 let cl = (cx * cx + cy * cy + cz * cz).sqrt().max(1e-12);
130 [cx / cl, cy / cl, cz / cl]
131 } else {
132 let cx = 0.0;
133 let cy = -dir[2];
134 let cz = dir[1];
135 let cl = (cx * cx + cy * cy + cz * cz).sqrt().max(1e-12);
136 [cx / cl, cy / cl, cz / cl]
137 };
138
139 let (freq, amp) = curl_params(profile, config);
140
141 let seed = (root[0].to_bits() as u64)
142 .wrapping_add(root[1].to_bits() as u64)
143 .wrapping_add(root[2].to_bits() as u64);
144 let mut rng = Lcg::new(seed);
145 let phase = rng.next_f32() * std::f32::consts::TAU;
146
147 let mut points = Vec::with_capacity(n * 3);
148 let mut px = root[0];
149 let mut py = root[1];
150 let mut pz = root[2];
151 points.push(px);
152 points.push(py);
153 points.push(pz);
154
155 for i in 1..n {
156 let t = i as f32 / (n - 1) as f32;
157 let angle = t * freq * std::f32::consts::TAU + phase;
158 let curl_offset = angle.sin() * amp;
159
160 px += dir[0] * seg_len + tangent[0] * curl_offset;
161 py += dir[1] * seg_len + tangent[1] * curl_offset;
162 pz += dir[2] * seg_len + tangent[2] * curl_offset;
163
164 points.push(px);
165 points.push(py);
166 points.push(pz);
167 }
168
169 let rest = compute_rest_length(&points);
170
171 HairStrand {
172 points,
173 profile,
174 rest_length: rest,
175 }
176}
177
178fn curl_params(profile: HairProfile, config: &HairStrandConfig) -> (f32, f32) {
179 match profile {
180 HairProfile::Straight => (0.0, 0.0),
181 HairProfile::Wavy => (config.curl_freq * 0.5, config.curl_amp * 0.5),
182 HairProfile::Curly => (config.curl_freq, config.curl_amp),
183 HairProfile::Coily => (config.curl_freq * 2.0, config.curl_amp * 1.5),
184 }
185}
186
187fn compute_rest_length(points: &[f32]) -> f32 {
188 let n = points.len() / 3;
189 if n < 2 {
190 return 0.0;
191 }
192 let mut total = 0.0_f32;
193 for i in 1..n {
194 let dx = points[i * 3] - points[(i - 1) * 3];
195 let dy = points[i * 3 + 1] - points[(i - 1) * 3 + 1];
196 let dz = points[i * 3 + 2] - points[(i - 1) * 3 + 2];
197 total += (dx * dx + dy * dy + dz * dz).sqrt();
198 }
199 total
200}
201
202#[allow(dead_code)]
209pub fn apply_gravity_to_strand(strand: &mut HairStrand, gravity: f32, dt: f32) {
210 let n = strand.points.len() / 3;
211 if n < 2 {
212 return;
213 }
214 for i in 1..n {
215 let factor = i as f32 / (n - 1) as f32;
216 strand.points[i * 3 + 1] -= gravity * dt * factor;
217 }
218}
219
220#[allow(dead_code)]
226pub fn strand_length(strand: &HairStrand) -> f32 {
227 compute_rest_length(&strand.points)
228}
229
230#[allow(dead_code)]
232pub fn strand_point_count(strand: &HairStrand) -> usize {
233 strand.points.len() / 3
234}
235
236#[allow(dead_code)]
238pub fn set_strand_profile(strand: &mut HairStrand, profile: HairProfile) {
239 strand.profile = profile;
240}
241
242#[allow(dead_code)]
244pub fn curl_frequency(profile: HairProfile, config: &HairStrandConfig) -> f32 {
245 curl_params(profile, config).0
246}
247
248#[allow(dead_code)]
250pub fn curl_amplitude(profile: HairProfile, config: &HairStrandConfig) -> f32 {
251 curl_params(profile, config).1
252}
253
254#[allow(dead_code)]
257pub fn strand_tangent_at(strand: &HairStrand, t: f32) -> [f32; 3] {
258 let n = strand.points.len() / 3;
259 if n < 2 {
260 return [0.0, 0.0, 0.0];
261 }
262 let t = t.clamp(0.0, 1.0);
263 let seg = (t * (n - 1) as f32).min((n - 2) as f32);
264 let idx = seg as usize;
265 let dx = strand.points[(idx + 1) * 3] - strand.points[idx * 3];
266 let dy = strand.points[(idx + 1) * 3 + 1] - strand.points[idx * 3 + 1];
267 let dz = strand.points[(idx + 1) * 3 + 2] - strand.points[idx * 3 + 2];
268 let len = (dx * dx + dy * dy + dz * dz).sqrt().max(1e-12);
269 [dx / len, dy / len, dz / len]
270}
271
272#[allow(dead_code)]
275pub fn blend_strands(a: &HairStrand, b: &HairStrand, t: f32) -> HairStrand {
276 let t = t.clamp(0.0, 1.0);
277 let count = a.points.len().min(b.points.len());
278 let mut points = Vec::with_capacity(count);
279 for i in 0..count {
280 points.push(a.points[i] + (b.points[i] - a.points[i]) * t);
281 }
282 let profile = if t < 0.5 { a.profile } else { b.profile };
283 let rest = compute_rest_length(&points);
284 HairStrand {
285 points,
286 profile,
287 rest_length: rest,
288 }
289}
290
291#[allow(dead_code)]
295pub fn strand_bounding_box(strand: &HairStrand) -> BoundingBox {
296 let n = strand.points.len() / 3;
297 if n == 0 {
298 return ([0.0; 3], [0.0; 3]);
299 }
300 let mut min = [strand.points[0], strand.points[1], strand.points[2]];
301 let mut max = min;
302 for i in 1..n {
303 for j in 0..3 {
304 let v = strand.points[i * 3 + j];
305 if v < min[j] {
306 min[j] = v;
307 }
308 if v > max[j] {
309 max[j] = v;
310 }
311 }
312 }
313 (min, max)
314}
315
316#[allow(dead_code)]
319pub fn strand_to_vertices(strand: &HairStrand) -> Vec<f32> {
320 strand.points.clone()
321}
322
323#[cfg(test)]
328mod tests {
329 use super::*;
330
331 #[test]
332 fn test_default_config() {
333 let cfg = default_strand_config();
334 assert!(cfg.point_count >= 2);
335 assert!(cfg.length > 0.0);
336 }
337
338 #[test]
339 fn test_new_strand_empty() {
340 let s = new_hair_strand(HairProfile::Straight);
341 assert!(s.points.is_empty());
342 assert_eq!(s.profile, HairProfile::Straight);
343 }
344
345 #[test]
346 fn test_generate_straight() {
347 let cfg = default_strand_config();
348 let s = generate_strand_points(
349 [0.0, 0.0, 0.0],
350 [0.0, -1.0, 0.0],
351 &cfg,
352 HairProfile::Straight,
353 );
354 assert_eq!(strand_point_count(&s), cfg.point_count);
355 assert!(strand_length(&s) > 0.0);
356 }
357
358 #[test]
359 fn test_generate_curly() {
360 let cfg = default_strand_config();
361 let s = generate_strand_points([0.0, 0.0, 0.0], [0.0, -1.0, 0.0], &cfg, HairProfile::Curly);
362 assert_eq!(strand_point_count(&s), cfg.point_count);
363 }
364
365 #[test]
366 fn test_generate_coily() {
367 let cfg = default_strand_config();
368 let s = generate_strand_points([0.0, 0.0, 0.0], [0.0, -1.0, 0.0], &cfg, HairProfile::Coily);
369 assert_eq!(strand_point_count(&s), cfg.point_count);
370 }
371
372 #[test]
373 fn test_gravity_displaces_downward() {
374 let cfg = default_strand_config();
375 let mut s = generate_strand_points(
376 [0.0, 1.0, 0.0],
377 [0.0, -1.0, 0.0],
378 &cfg,
379 HairProfile::Straight,
380 );
381 let y_before = s.points[s.points.len() - 2]; apply_gravity_to_strand(&mut s, 9.81, 0.1);
383 let y_after = s.points[s.points.len() - 2];
384 assert!(y_after < y_before);
385 }
386
387 #[test]
388 fn test_strand_length_positive() {
389 let cfg = default_strand_config();
390 let s = generate_strand_points(
391 [0.0, 0.0, 0.0],
392 [0.0, -1.0, 0.0],
393 &cfg,
394 HairProfile::Straight,
395 );
396 assert!(strand_length(&s) > 0.0);
397 }
398
399 #[test]
400 fn test_strand_point_count() {
401 let cfg = default_strand_config();
402 let s = generate_strand_points([0.0, 0.0, 0.0], [0.0, -1.0, 0.0], &cfg, HairProfile::Wavy);
403 assert_eq!(strand_point_count(&s), cfg.point_count);
404 }
405
406 #[test]
407 fn test_set_profile() {
408 let mut s = new_hair_strand(HairProfile::Straight);
409 set_strand_profile(&mut s, HairProfile::Curly);
410 assert_eq!(s.profile, HairProfile::Curly);
411 }
412
413 #[test]
414 fn test_curl_frequency_straight_zero() {
415 let cfg = default_strand_config();
416 assert_eq!(curl_frequency(HairProfile::Straight, &cfg), 0.0);
417 }
418
419 #[test]
420 fn test_curl_amplitude_curly_positive() {
421 let cfg = default_strand_config();
422 assert!(curl_amplitude(HairProfile::Curly, &cfg) > 0.0);
423 }
424
425 #[test]
426 fn test_tangent_at_start() {
427 let cfg = default_strand_config();
428 let s = generate_strand_points(
429 [0.0, 0.0, 0.0],
430 [0.0, -1.0, 0.0],
431 &cfg,
432 HairProfile::Straight,
433 );
434 let tan = strand_tangent_at(&s, 0.0);
435 assert!(tan[1] < 0.0);
437 }
438
439 #[test]
440 fn test_tangent_empty_strand() {
441 let s = new_hair_strand(HairProfile::Straight);
442 let tan = strand_tangent_at(&s, 0.5);
443 assert_eq!(tan, [0.0, 0.0, 0.0]);
444 }
445
446 #[test]
447 fn test_blend_strands_at_zero() {
448 let cfg = default_strand_config();
449 let a = generate_strand_points(
450 [0.0, 0.0, 0.0],
451 [0.0, -1.0, 0.0],
452 &cfg,
453 HairProfile::Straight,
454 );
455 let b = generate_strand_points([1.0, 0.0, 0.0], [0.0, -1.0, 0.0], &cfg, HairProfile::Curly);
456 let c = blend_strands(&a, &b, 0.0);
457 assert_eq!(c.points[0], a.points[0]);
458 assert_eq!(c.profile, HairProfile::Straight);
459 }
460
461 #[test]
462 fn test_bounding_box_non_empty() {
463 let cfg = default_strand_config();
464 let s = generate_strand_points(
465 [0.0, 0.0, 0.0],
466 [0.0, -1.0, 0.0],
467 &cfg,
468 HairProfile::Straight,
469 );
470 let (min, max) = strand_bounding_box(&s);
471 assert!(max[1] >= min[1]);
472 }
473
474 #[test]
475 fn test_bounding_box_empty() {
476 let s = new_hair_strand(HairProfile::Straight);
477 let (min, max) = strand_bounding_box(&s);
478 assert_eq!(min, [0.0, 0.0, 0.0]);
479 assert_eq!(max, [0.0, 0.0, 0.0]);
480 }
481
482 #[test]
483 fn test_strand_to_vertices() {
484 let cfg = default_strand_config();
485 let s = generate_strand_points(
486 [0.0, 0.0, 0.0],
487 [0.0, -1.0, 0.0],
488 &cfg,
489 HairProfile::Straight,
490 );
491 let verts = strand_to_vertices(&s);
492 assert_eq!(verts.len(), s.points.len());
493 }
494}