1#[allow(dead_code)]
8#[derive(Debug, Clone)]
9pub struct RetargetMeshConfig {
10 pub search_radius: f32,
12 pub smooth_iterations: u32,
14 pub blend: f32,
16}
17
18impl Default for RetargetMeshConfig {
19 fn default() -> Self {
20 Self {
21 search_radius: 0.1,
22 smooth_iterations: 2,
23 blend: 1.0,
24 }
25 }
26}
27
28#[allow(dead_code)]
30#[derive(Debug, Clone)]
31pub struct RetargetMeshResult {
32 pub positions: Vec<[f32; 3]>,
34 pub transferred_count: usize,
36 pub failed_count: usize,
38 pub avg_error: f32,
40}
41
42#[allow(dead_code)]
45pub fn closest_vertex(
46 query: [f32; 3],
47 positions: &[[f32; 3]],
48 max_dist: f32,
49) -> Option<(usize, f32)> {
50 let mut best_idx = None;
51 let mut best_dist = max_dist;
52 for (i, &p) in positions.iter().enumerate() {
53 let d = dist3(query, p);
54 if d < best_dist {
55 best_dist = d;
56 best_idx = Some(i);
57 }
58 }
59 best_idx.map(|i| (i, best_dist))
60}
61
62#[allow(dead_code)]
68pub fn retarget_mesh_positions(
69 source: &[[f32; 3]],
70 target_base: &[[f32; 3]],
71 target_deformed: &[[f32; 3]],
72 cfg: &RetargetMeshConfig,
73) -> RetargetMeshResult {
74 let n = source.len();
75 let mut positions = Vec::with_capacity(n);
76 let mut failed_mask = Vec::with_capacity(n);
77 let mut transferred_count = 0usize;
78 let mut failed_count = 0usize;
79 let mut error_sum = 0.0f32;
80
81 for &sv in source.iter() {
82 match closest_vertex(sv, target_base, cfg.search_radius) {
83 Some((idx, d)) => {
84 let delta = sub3(target_deformed[idx], target_base[idx]);
85 let scaled = scale3(delta, cfg.blend);
86 positions.push(add3(sv, scaled));
87 failed_mask.push(false);
88 transferred_count += 1;
89 error_sum += d;
90 }
91 None => {
92 positions.push(sv);
93 failed_mask.push(true);
94 failed_count += 1;
95 }
96 }
97 }
98
99 let avg_error = if transferred_count > 0 {
100 error_sum / transferred_count as f32
101 } else {
102 0.0
103 };
104
105 let _ = cfg.smooth_iterations; RetargetMeshResult {
110 positions,
111 transferred_count,
112 failed_count,
113 avg_error,
114 }
115}
116
117#[allow(dead_code)]
122pub fn transfer_deltas(
123 source: &[[f32; 3]],
124 source_base: &[[f32; 3]],
125 target_base: &[[f32; 3]],
126 target_deltas: &[[f32; 3]],
127 cfg: &RetargetMeshConfig,
128) -> Vec<[f32; 3]> {
129 let _ = source_base; source
131 .iter()
132 .map(
133 |&sv| match closest_vertex(sv, target_base, cfg.search_radius) {
134 Some((idx, _)) => scale3(target_deltas[idx], cfg.blend),
135 None => [0.0, 0.0, 0.0],
136 },
137 )
138 .collect()
139}
140
141#[allow(dead_code)]
146pub fn smooth_transferred_positions(
147 positions: &[[f32; 3]],
148 failed_mask: &[bool],
149 indices: &[u32],
150 iterations: u32,
151) -> Vec<[f32; 3]> {
152 let n = positions.len();
153 let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
155 for tri in indices.chunks_exact(3) {
156 let (a, b, c) = (tri[0] as usize, tri[1] as usize, tri[2] as usize);
157 if !adj[a].contains(&b) {
158 adj[a].push(b);
159 }
160 if !adj[a].contains(&c) {
161 adj[a].push(c);
162 }
163 if !adj[b].contains(&a) {
164 adj[b].push(a);
165 }
166 if !adj[b].contains(&c) {
167 adj[b].push(c);
168 }
169 if !adj[c].contains(&a) {
170 adj[c].push(a);
171 }
172 if !adj[c].contains(&b) {
173 adj[c].push(b);
174 }
175 }
176
177 let mut current: Vec<[f32; 3]> = positions.to_vec();
178 for _ in 0..iterations {
179 let prev = current.clone();
180 for (i, cur_pos) in current.iter_mut().enumerate() {
181 if failed_mask.get(i).copied().unwrap_or(false) && !adj[i].is_empty() {
182 let mut sum = [0.0f32; 3];
183 for &nb in &adj[i] {
184 sum = add3(sum, prev[nb]);
185 }
186 let cnt = adj[i].len() as f32;
187 *cur_pos = [sum[0] / cnt, sum[1] / cnt, sum[2] / cnt];
188 }
189 }
190 }
191 current
192}
193
194#[allow(dead_code)]
196pub fn retarget_error_stats(result: &RetargetMeshResult) -> String {
197 let total = result.transferred_count + result.failed_count;
198 let coverage = if total > 0 {
199 result.transferred_count as f32 / total as f32 * 100.0
200 } else {
201 0.0
202 };
203 format!(
204 "transferred={} failed={} coverage={:.1}% avg_error={:.6}",
205 result.transferred_count, result.failed_count, coverage, result.avg_error
206 )
207}
208
209#[inline]
212fn dist3(a: [f32; 3], b: [f32; 3]) -> f32 {
213 let dx = a[0] - b[0];
214 let dy = a[1] - b[1];
215 let dz = a[2] - b[2];
216 (dx * dx + dy * dy + dz * dz).sqrt()
217}
218
219#[inline]
220fn sub3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
221 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
222}
223
224#[inline]
225fn add3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
226 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
227}
228
229#[inline]
230fn scale3(v: [f32; 3], s: f32) -> [f32; 3] {
231 [v[0] * s, v[1] * s, v[2] * s]
232}
233
234#[cfg(test)]
237mod tests {
238 use super::*;
239
240 fn cfg_default() -> RetargetMeshConfig {
241 RetargetMeshConfig::default()
242 }
243
244 #[test]
246 fn test_closest_vertex_finds_nearest() {
247 let pts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 0.0, 0.0]];
248 let (idx, d) = closest_vertex([0.49, 0.0, 0.0], &pts, 1.0).expect("should succeed");
249 assert_eq!(idx, 2);
250 assert!(d < 0.02);
251 }
252
253 #[test]
255 fn test_closest_vertex_none_beyond_radius() {
256 let pts = vec![[10.0, 0.0, 0.0]];
257 assert!(closest_vertex([0.0, 0.0, 0.0], &pts, 0.5).is_none());
258 }
259
260 #[test]
262 fn test_closest_vertex_exact() {
263 let pts = vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
264 let (idx, d) = closest_vertex([1.0, 2.0, 3.0], &pts, 0.001).expect("should succeed");
265 assert_eq!(idx, 0);
266 assert!(d < 1e-6);
267 }
268
269 #[test]
271 fn test_retarget_identity_zero_delta() {
272 let verts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
273 let cfg = RetargetMeshConfig {
274 search_radius: 1.0,
275 ..cfg_default()
276 };
277 let result = retarget_mesh_positions(&verts, &verts, &verts, &cfg);
278 for (orig, out) in verts.iter().zip(result.positions.iter()) {
279 assert!((orig[0] - out[0]).abs() < 1e-6);
280 assert!((orig[1] - out[1]).abs() < 1e-6);
281 assert!((orig[2] - out[2]).abs() < 1e-6);
282 }
283 assert_eq!(result.failed_count, 0);
284 }
285
286 #[test]
288 fn test_retarget_applies_delta() {
289 let source = vec![[0.0, 0.0, 0.0]];
290 let target_base = vec![[0.0, 0.0, 0.0]];
291 let target_deformed = vec![[0.0, 1.0, 0.0]];
292 let cfg = RetargetMeshConfig {
293 search_radius: 0.5,
294 blend: 1.0,
295 ..cfg_default()
296 };
297 let result = retarget_mesh_positions(&source, &target_base, &target_deformed, &cfg);
298 assert!((result.positions[0][1] - 1.0).abs() < 1e-6);
299 }
300
301 #[test]
303 fn test_retarget_blend_zero_no_change() {
304 let source = vec![[0.0, 0.0, 0.0]];
305 let target_base = vec![[0.0, 0.0, 0.0]];
306 let target_deformed = vec![[0.0, 5.0, 0.0]];
307 let cfg = RetargetMeshConfig {
308 search_radius: 0.5,
309 blend: 0.0,
310 ..cfg_default()
311 };
312 let result = retarget_mesh_positions(&source, &target_base, &target_deformed, &cfg);
313 assert!((result.positions[0][1]).abs() < 1e-6);
314 }
315
316 #[test]
318 fn test_retarget_blend_one_full() {
319 let source = vec![[0.0, 0.0, 0.0]];
320 let target_base = vec![[0.0, 0.0, 0.0]];
321 let target_deformed = vec![[3.0, 0.0, 0.0]];
322 let cfg = RetargetMeshConfig {
323 search_radius: 0.5,
324 blend: 1.0,
325 ..cfg_default()
326 };
327 let result = retarget_mesh_positions(&source, &target_base, &target_deformed, &cfg);
328 assert!((result.positions[0][0] - 3.0).abs() < 1e-6);
329 }
330
331 #[test]
333 fn test_retarget_failed_count() {
334 let source = vec![[0.0, 0.0, 0.0], [100.0, 0.0, 0.0]];
335 let target_base = vec![[0.0, 0.0, 0.0]];
336 let target_deformed = vec![[0.0, 1.0, 0.0]];
337 let cfg = RetargetMeshConfig {
338 search_radius: 0.5,
339 ..cfg_default()
340 };
341 let result = retarget_mesh_positions(&source, &target_base, &target_deformed, &cfg);
342 assert_eq!(result.failed_count, 1);
343 assert_eq!(result.transferred_count, 1);
344 }
345
346 #[test]
348 fn test_transfer_deltas_count_matches_source() {
349 let source = vec![[0.0f32; 3]; 5];
350 let source_base = vec![[0.0f32; 3]; 5];
351 let target_base = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
352 let target_deltas = vec![[0.1, 0.0, 0.0], [0.2, 0.0, 0.0]];
353 let cfg = RetargetMeshConfig {
354 search_radius: 2.0,
355 ..cfg_default()
356 };
357 let out = transfer_deltas(&source, &source_base, &target_base, &target_deltas, &cfg);
358 assert_eq!(out.len(), 5);
359 }
360
361 #[test]
363 fn test_transfer_deltas_value() {
364 let source = vec![[0.0, 0.0, 0.0]];
365 let source_base = vec![[0.0, 0.0, 0.0]];
366 let target_base = vec![[0.0, 0.0, 0.0]];
367 let target_deltas = vec![[0.5, 0.25, 0.1]];
368 let cfg = RetargetMeshConfig {
369 search_radius: 1.0,
370 blend: 1.0,
371 ..cfg_default()
372 };
373 let out = transfer_deltas(&source, &source_base, &target_base, &target_deltas, &cfg);
374 assert!((out[0][0] - 0.5).abs() < 1e-6);
375 }
376
377 #[test]
379 fn test_smooth_no_failures_noop() {
380 let positions = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0]];
381 let failed_mask = vec![false, false, false];
382 let indices = vec![0u32, 1, 2];
383 let out = smooth_transferred_positions(&positions, &failed_mask, &indices, 3);
384 for (a, b) in positions.iter().zip(out.iter()) {
385 assert!((a[0] - b[0]).abs() < 1e-6);
386 assert!((a[1] - b[1]).abs() < 1e-6);
387 assert!((a[2] - b[2]).abs() < 1e-6);
388 }
389 }
390
391 #[test]
393 fn test_smooth_failed_vertex_moves() {
394 let positions = vec![[0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [99.0, 99.0, 99.0]];
395 let failed_mask = vec![false, false, true];
396 let indices = vec![0u32, 1, 2];
397 let out = smooth_transferred_positions(&positions, &failed_mask, &indices, 1);
398 assert!((out[2][0] - 1.0).abs() < 1e-5);
400 }
401
402 #[test]
404 fn test_avg_error_computed() {
405 let source = vec![[0.05, 0.0, 0.0]];
406 let target_base = vec![[0.0, 0.0, 0.0]];
407 let target_deformed = vec![[0.0, 0.1, 0.0]];
408 let cfg = RetargetMeshConfig {
409 search_radius: 1.0,
410 ..cfg_default()
411 };
412 let result = retarget_mesh_positions(&source, &target_base, &target_deformed, &cfg);
413 assert!(result.avg_error >= 0.0);
414 assert!(result.avg_error < 1.0);
415 }
416
417 #[test]
419 fn test_retarget_error_stats_format() {
420 let result = RetargetMeshResult {
421 positions: vec![],
422 transferred_count: 8,
423 failed_count: 2,
424 avg_error: 0.01234,
425 };
426 let s = retarget_error_stats(&result);
427 assert!(s.contains("transferred=8"));
428 assert!(s.contains("failed=2"));
429 assert!(s.contains("avg_error"));
430 }
431
432 #[test]
434 fn test_retarget_error_stats_zero_total() {
435 let result = RetargetMeshResult {
436 positions: vec![],
437 transferred_count: 0,
438 failed_count: 0,
439 avg_error: 0.0,
440 };
441 let s = retarget_error_stats(&result);
442 assert!(s.contains("coverage=0.0%"));
443 }
444}