Skip to main content

oxihuman_viewer/
occlusion_map.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Occlusion map — ambient occlusion texture / buffer for GI approximation.
6
7/// Ambient occlusion algorithm.
8#[allow(dead_code)]
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum AoAlgorithm {
11    Ssao,
12    Hbao,
13    Gtao,
14    BakedTexture,
15}
16
17/// Occlusion map configuration.
18#[allow(dead_code)]
19#[derive(Debug, Clone)]
20pub struct OcclusionMapConfig {
21    pub algorithm: AoAlgorithm,
22    pub radius: f32,
23    pub bias: f32,
24    pub intensity: f32,
25    pub sample_count: u32,
26    pub enabled: bool,
27}
28
29/// Flat occlusion buffer (per-pixel values in [0, 1]).
30#[allow(dead_code)]
31#[derive(Debug, Clone)]
32pub struct OcclusionBuffer {
33    pub width: usize,
34    pub height: usize,
35    pub data: Vec<f32>,
36}
37
38#[allow(dead_code)]
39pub fn default_occlusion_map_config() -> OcclusionMapConfig {
40    OcclusionMapConfig {
41        algorithm: AoAlgorithm::Ssao,
42        radius: 0.5,
43        bias: 0.025,
44        intensity: 1.5,
45        sample_count: 16,
46        enabled: true,
47    }
48}
49
50#[allow(dead_code)]
51pub fn new_occlusion_buffer(width: usize, height: usize) -> OcclusionBuffer {
52    OcclusionBuffer {
53        width,
54        height,
55        data: vec![1.0; width * height],
56    }
57}
58
59#[allow(dead_code)]
60pub fn om_set_pixel(buf: &mut OcclusionBuffer, x: usize, y: usize, value: f32) {
61    if x < buf.width && y < buf.height {
62        let idx = y * buf.width + x;
63        buf.data[idx] = value.clamp(0.0, 1.0);
64    }
65}
66
67#[allow(dead_code)]
68pub fn om_get_pixel(buf: &OcclusionBuffer, x: usize, y: usize) -> f32 {
69    if x < buf.width && y < buf.height {
70        buf.data[y * buf.width + x]
71    } else {
72        1.0
73    }
74}
75
76#[allow(dead_code)]
77pub fn om_clear(buf: &mut OcclusionBuffer) {
78    for v in buf.data.iter_mut() {
79        *v = 1.0;
80    }
81}
82
83#[allow(dead_code)]
84pub fn om_average(buf: &OcclusionBuffer) -> f32 {
85    if buf.data.is_empty() {
86        return 1.0;
87    }
88    buf.data.iter().sum::<f32>() / buf.data.len() as f32
89}
90
91#[allow(dead_code)]
92pub fn om_apply_intensity(cfg: &OcclusionMapConfig, raw_ao: f32) -> f32 {
93    if !cfg.enabled {
94        return 1.0;
95    }
96    raw_ao.powf(cfg.intensity).clamp(0.0, 1.0)
97}
98
99#[allow(dead_code)]
100pub fn om_set_intensity(cfg: &mut OcclusionMapConfig, v: f32) {
101    cfg.intensity = v.clamp(0.0, 5.0);
102}
103
104#[allow(dead_code)]
105pub fn om_set_radius(cfg: &mut OcclusionMapConfig, v: f32) {
106    cfg.radius = v.clamp(0.001, 10.0);
107}
108
109#[allow(dead_code)]
110pub fn om_to_json(cfg: &OcclusionMapConfig) -> String {
111    let algo = match cfg.algorithm {
112        AoAlgorithm::Ssao => "ssao",
113        AoAlgorithm::Hbao => "hbao",
114        AoAlgorithm::Gtao => "gtao",
115        AoAlgorithm::BakedTexture => "baked",
116    };
117    format!(
118        r#"{{"algorithm":"{}","radius":{:.4},"intensity":{:.4},"enabled":{}}}"#,
119        algo, cfg.radius, cfg.intensity, cfg.enabled
120    )
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn default_config_enabled() {
129        let cfg = default_occlusion_map_config();
130        assert!(cfg.enabled);
131    }
132
133    #[test]
134    fn new_buffer_all_ones() {
135        let buf = new_occlusion_buffer(4, 4);
136        assert!(buf.data.iter().all(|&v| (v - 1.0).abs() < 1e-6));
137    }
138
139    #[test]
140    fn set_and_get_pixel() {
141        let mut buf = new_occlusion_buffer(4, 4);
142        om_set_pixel(&mut buf, 1, 2, 0.5);
143        assert!((om_get_pixel(&buf, 1, 2) - 0.5).abs() < 1e-6);
144    }
145
146    #[test]
147    fn get_out_of_bounds_returns_one() {
148        let buf = new_occlusion_buffer(2, 2);
149        assert!((om_get_pixel(&buf, 10, 10) - 1.0).abs() < 1e-6);
150    }
151
152    #[test]
153    fn clear_resets() {
154        let mut buf = new_occlusion_buffer(2, 2);
155        om_set_pixel(&mut buf, 0, 0, 0.0);
156        om_clear(&mut buf);
157        assert!((om_average(&buf) - 1.0).abs() < 1e-6);
158    }
159
160    #[test]
161    fn average_computed() {
162        let mut buf = new_occlusion_buffer(2, 1);
163        om_set_pixel(&mut buf, 0, 0, 0.0);
164        om_set_pixel(&mut buf, 1, 0, 1.0);
165        assert!((om_average(&buf) - 0.5).abs() < 1e-6);
166    }
167
168    #[test]
169    fn apply_intensity_disabled() {
170        let mut cfg = default_occlusion_map_config();
171        cfg.enabled = false;
172        assert!((om_apply_intensity(&cfg, 0.3) - 1.0).abs() < 1e-6);
173    }
174
175    #[test]
176    fn set_intensity_clamps() {
177        let mut cfg = default_occlusion_map_config();
178        om_set_intensity(&mut cfg, 100.0);
179        assert!((cfg.intensity - 5.0).abs() < 1e-6);
180    }
181
182    #[test]
183    fn set_radius_clamps() {
184        let mut cfg = default_occlusion_map_config();
185        om_set_radius(&mut cfg, 0.0);
186        assert!(cfg.radius > 0.0);
187    }
188
189    #[test]
190    fn to_json_fields() {
191        let cfg = default_occlusion_map_config();
192        let j = om_to_json(&cfg);
193        assert!(j.contains("algorithm"));
194        assert!(j.contains("intensity"));
195    }
196}