Skip to main content

oxihuman_viewer/
motion_vector.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Motion-vector buffer management for temporal effects (TAA, motion blur).
6
7/// A per-pixel motion vector (screen-space displacement in UV space).
8#[allow(dead_code)]
9#[derive(Clone, Copy, Debug, Default)]
10pub struct MotionVector {
11    pub du: f32,
12    pub dv: f32,
13}
14
15impl MotionVector {
16    #[allow(dead_code)]
17    pub fn magnitude(&self) -> f32 {
18        (self.du * self.du + self.dv * self.dv).sqrt()
19    }
20}
21
22/// Motion vector buffer.
23#[allow(dead_code)]
24#[derive(Clone, Debug)]
25pub struct MotionVectorBuffer {
26    pub width: u32,
27    pub height: u32,
28    pub vectors: Vec<MotionVector>,
29}
30
31impl MotionVectorBuffer {
32    #[allow(dead_code)]
33    pub fn new(width: u32, height: u32) -> Self {
34        let count = (width * height) as usize;
35        MotionVectorBuffer {
36            width,
37            height,
38            vectors: vec![MotionVector::default(); count],
39        }
40    }
41}
42
43#[allow(dead_code)]
44pub fn mv_set(buf: &mut MotionVectorBuffer, x: u32, y: u32, du: f32, dv: f32) {
45    if x < buf.width && y < buf.height {
46        let idx = (y * buf.width + x) as usize;
47        buf.vectors[idx] = MotionVector { du, dv };
48    }
49}
50
51#[allow(dead_code)]
52pub fn mv_get(buf: &MotionVectorBuffer, x: u32, y: u32) -> Option<MotionVector> {
53    if x < buf.width && y < buf.height {
54        Some(buf.vectors[(y * buf.width + x) as usize])
55    } else {
56        None
57    }
58}
59
60#[allow(dead_code)]
61pub fn mv_clear(buf: &mut MotionVectorBuffer) {
62    for v in &mut buf.vectors {
63        *v = MotionVector::default();
64    }
65}
66
67#[allow(dead_code)]
68pub fn mv_max_magnitude(buf: &MotionVectorBuffer) -> f32 {
69    buf.vectors
70        .iter()
71        .map(|v| v.magnitude())
72        .fold(0.0f32, f32::max)
73}
74
75#[allow(dead_code)]
76pub fn mv_average_magnitude(buf: &MotionVectorBuffer) -> f32 {
77    if buf.vectors.is_empty() {
78        return 0.0;
79    }
80    buf.vectors.iter().map(|v| v.magnitude()).sum::<f32>() / buf.vectors.len() as f32
81}
82
83#[allow(dead_code)]
84pub fn mv_pixel_count(buf: &MotionVectorBuffer) -> usize {
85    buf.vectors.len()
86}
87
88#[allow(dead_code)]
89pub fn mv_to_json(buf: &MotionVectorBuffer) -> String {
90    format!(
91        "{{\"width\":{},\"height\":{},\"max_mag\":{:.4}}}",
92        buf.width,
93        buf.height,
94        mv_max_magnitude(buf)
95    )
96}
97
98#[allow(dead_code)]
99pub fn mv_scale(buf: &mut MotionVectorBuffer, scale: f32) {
100    for v in &mut buf.vectors {
101        v.du *= scale;
102        v.dv *= scale;
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn new_all_zero() {
112        let b = MotionVectorBuffer::new(4, 4);
113        assert!((mv_max_magnitude(&b)).abs() < 1e-5);
114    }
115
116    #[test]
117    fn set_and_get() {
118        let mut b = MotionVectorBuffer::new(8, 8);
119        mv_set(&mut b, 2, 3, 0.1, 0.2);
120        let v = mv_get(&b, 2, 3).expect("should succeed");
121        assert!((v.du - 0.1).abs() < 1e-5);
122    }
123
124    #[test]
125    fn out_of_bounds_get_none() {
126        let b = MotionVectorBuffer::new(4, 4);
127        assert!(mv_get(&b, 10, 10).is_none());
128    }
129
130    #[test]
131    fn clear_zeroes() {
132        let mut b = MotionVectorBuffer::new(2, 2);
133        mv_set(&mut b, 0, 0, 1.0, 1.0);
134        mv_clear(&mut b);
135        assert!((mv_max_magnitude(&b)).abs() < 1e-5);
136    }
137
138    #[test]
139    fn magnitude_formula() {
140        let v = MotionVector { du: 3.0, dv: 4.0 };
141        assert!((v.magnitude() - 5.0).abs() < 1e-4);
142    }
143
144    #[test]
145    fn pixel_count() {
146        let b = MotionVectorBuffer::new(10, 10);
147        assert_eq!(mv_pixel_count(&b), 100);
148    }
149
150    #[test]
151    fn scale_doubles() {
152        let mut b = MotionVectorBuffer::new(2, 2);
153        mv_set(&mut b, 0, 0, 1.0, 0.0);
154        mv_scale(&mut b, 2.0);
155        let v = mv_get(&b, 0, 0).expect("should succeed");
156        assert!((v.du - 2.0).abs() < 1e-5);
157    }
158
159    #[test]
160    fn average_magnitude_uniform() {
161        let mut b = MotionVectorBuffer::new(1, 1);
162        mv_set(&mut b, 0, 0, 1.0, 0.0);
163        assert!((mv_average_magnitude(&b) - 1.0).abs() < 1e-5);
164    }
165
166    #[test]
167    fn json_has_width() {
168        assert!(mv_to_json(&MotionVectorBuffer::new(3, 3)).contains("width"));
169    }
170
171    #[test]
172    fn out_of_bounds_set_ignored() {
173        let mut b = MotionVectorBuffer::new(4, 4);
174        mv_set(&mut b, 99, 99, 1.0, 1.0); // should not panic
175        assert_eq!(mv_pixel_count(&b), 16);
176    }
177}