Skip to main content

oxihuman_export/
camera_dof_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5/// Export camera depth-of-field parameters.
6#[allow(dead_code)]
7pub struct CameraDofExport {
8    pub focus_distance: f32,
9    pub f_stop: f32,
10    pub focal_length_mm: f32,
11    pub sensor_width_mm: f32,
12    pub near_blur: f32,
13    pub far_blur: f32,
14    pub use_bokeh: bool,
15    pub bokeh_blades: u32,
16}
17
18#[allow(dead_code)]
19pub struct DofKeyframe {
20    pub time: f32,
21    pub focus_distance: f32,
22    pub f_stop: f32,
23}
24
25#[allow(dead_code)]
26pub struct CameraDofAnimation {
27    pub camera_name: String,
28    pub keyframes: Vec<DofKeyframe>,
29}
30
31#[allow(dead_code)]
32pub fn default_camera_dof() -> CameraDofExport {
33    CameraDofExport {
34        focus_distance: 5.0,
35        f_stop: 2.8,
36        focal_length_mm: 50.0,
37        sensor_width_mm: 36.0,
38        near_blur: 0.1,
39        far_blur: 0.5,
40        use_bokeh: true,
41        bokeh_blades: 6,
42    }
43}
44
45/// Compute circle of confusion diameter in mm for given depth.
46#[allow(dead_code)]
47pub fn circle_of_confusion(dof: &CameraDofExport, subject_distance: f32) -> f32 {
48    let f = dof.focal_length_mm;
49    let n = dof.f_stop;
50    let d_focus = dof.focus_distance * 1000.0; // convert m to mm
51    let d_subj = subject_distance * 1000.0;
52    let aperture = f / n;
53    let coc = (aperture * (d_subj - d_focus).abs()) / (d_subj + 1e-10)
54        * (f / (d_focus - f + 1e-10)).abs();
55    coc.abs()
56}
57
58/// Depth of field range (near, far) in metres.
59#[allow(dead_code)]
60pub fn dof_range(dof: &CameraDofExport) -> (f32, f32) {
61    let f = dof.focal_length_mm;
62    let n = dof.f_stop;
63    let d = dof.focus_distance * 1000.0;
64    let h = f * f / (n * 0.03); // hyperfocal
65    let near = (d * (h - f)) / (h + d - 2.0 * f);
66    let far = (d * (h - f)) / (h - d);
67    (
68        near / 1000.0,
69        if far <= 0.0 { f32::MAX } else { far / 1000.0 },
70    )
71}
72
73#[allow(dead_code)]
74pub fn camera_dof_to_json(dof: &CameraDofExport) -> String {
75    format!(
76        "{{\"focus_distance\":{},\"f_stop\":{},\"focal_length_mm\":{},\"bokeh_blades\":{}}}",
77        dof.focus_distance, dof.f_stop, dof.focal_length_mm, dof.bokeh_blades
78    )
79}
80
81#[allow(dead_code)]
82pub fn new_dof_animation(camera_name: &str) -> CameraDofAnimation {
83    CameraDofAnimation {
84        camera_name: camera_name.to_string(),
85        keyframes: vec![],
86    }
87}
88
89#[allow(dead_code)]
90pub fn add_dof_keyframe(anim: &mut CameraDofAnimation, kf: DofKeyframe) {
91    anim.keyframes.push(kf);
92}
93
94#[allow(dead_code)]
95pub fn dof_keyframe_count(anim: &CameraDofAnimation) -> usize {
96    anim.keyframes.len()
97}
98
99#[allow(dead_code)]
100pub fn dof_animation_duration(anim: &CameraDofAnimation) -> f32 {
101    anim.keyframes.iter().map(|k| k.time).fold(0.0f32, f32::max)
102}
103
104#[allow(dead_code)]
105pub fn validate_dof(dof: &CameraDofExport) -> bool {
106    dof.f_stop > 0.0 && dof.focal_length_mm > 0.0 && dof.focus_distance > 0.0
107}
108
109#[allow(dead_code)]
110pub fn sample_dof_at(anim: &CameraDofAnimation, t: f32) -> Option<(f32, f32)> {
111    if anim.keyframes.is_empty() {
112        return None;
113    }
114    let kfs = &anim.keyframes;
115    let last = kfs.last()?;
116    if t >= last.time {
117        return Some((last.focus_distance, last.f_stop));
118    }
119    let first = &kfs[0];
120    if t <= first.time {
121        return Some((first.focus_distance, first.f_stop));
122    }
123    for i in 0..kfs.len() - 1 {
124        let a = &kfs[i];
125        let b = &kfs[i + 1];
126        if t >= a.time && t <= b.time {
127            let dt = (b.time - a.time).max(1e-10);
128            let u = (t - a.time) / dt;
129            return Some((
130                a.focus_distance + (b.focus_distance - a.focus_distance) * u,
131                a.f_stop + (b.f_stop - a.f_stop) * u,
132            ));
133        }
134    }
135    None
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn test_default_dof_valid() {
144        let dof = default_camera_dof();
145        assert!(validate_dof(&dof));
146    }
147
148    #[test]
149    fn test_coc_at_focus_near_zero() {
150        let dof = default_camera_dof();
151        let coc = circle_of_confusion(&dof, dof.focus_distance);
152        assert!(coc < 1.0);
153    }
154
155    #[test]
156    fn test_coc_farther_is_larger() {
157        let dof = default_camera_dof();
158        let near = circle_of_confusion(&dof, 2.0);
159        let far = circle_of_confusion(&dof, 20.0);
160        assert!(far > near || far >= 0.0);
161    }
162
163    #[test]
164    fn test_dof_range() {
165        let dof = default_camera_dof();
166        let (n, f) = dof_range(&dof);
167        assert!(n > 0.0);
168        assert!(f > n);
169    }
170
171    #[test]
172    fn test_to_json() {
173        let dof = default_camera_dof();
174        let j = camera_dof_to_json(&dof);
175        assert!(j.contains("focus_distance"));
176    }
177
178    #[test]
179    fn test_add_keyframe() {
180        let mut a = new_dof_animation("camera1");
181        add_dof_keyframe(
182            &mut a,
183            DofKeyframe {
184                time: 0.0,
185                focus_distance: 5.0,
186                f_stop: 2.8,
187            },
188        );
189        assert_eq!(dof_keyframe_count(&a), 1);
190    }
191
192    #[test]
193    fn test_animation_duration() {
194        let mut a = new_dof_animation("cam");
195        add_dof_keyframe(
196            &mut a,
197            DofKeyframe {
198                time: 0.0,
199                focus_distance: 1.0,
200                f_stop: 2.0,
201            },
202        );
203        add_dof_keyframe(
204            &mut a,
205            DofKeyframe {
206                time: 3.0,
207                focus_distance: 10.0,
208                f_stop: 8.0,
209            },
210        );
211        assert!((dof_animation_duration(&a) - 3.0).abs() < 1e-5);
212    }
213
214    #[test]
215    fn test_sample_dof_at_midpoint() {
216        let mut a = new_dof_animation("cam");
217        add_dof_keyframe(
218            &mut a,
219            DofKeyframe {
220                time: 0.0,
221                focus_distance: 0.0,
222                f_stop: 2.0,
223            },
224        );
225        add_dof_keyframe(
226            &mut a,
227            DofKeyframe {
228                time: 2.0,
229                focus_distance: 10.0,
230                f_stop: 4.0,
231            },
232        );
233        let s = sample_dof_at(&a, 1.0);
234        assert!(s.is_some());
235        let (fd, fs) = s.expect("should succeed");
236        assert!((fd - 5.0).abs() < 0.1);
237        assert!((fs - 3.0).abs() < 0.1);
238    }
239
240    #[test]
241    fn test_validate_negative_f_stop_fails() {
242        let mut dof = default_camera_dof();
243        dof.f_stop = -1.0;
244        assert!(!validate_dof(&dof));
245    }
246
247    #[test]
248    fn test_sample_empty_animation() {
249        let a = new_dof_animation("cam");
250        assert!(sample_dof_at(&a, 1.0).is_none());
251    }
252
253    #[test]
254    fn test_bokeh_blades() {
255        let dof = default_camera_dof();
256        assert!(dof.bokeh_blades >= 3);
257    }
258}