#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackgroundType {
SolidColor,
VerticalGradient,
HorizontalGradient,
Checkerboard,
Cubemap,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct BackgroundConfig {
pub bg_type: BackgroundType,
pub top_color: [f32; 4],
pub bottom_color: [f32; 4],
pub checker_size: u32,
}
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub struct BackgroundSample {
pub color: [f32; 4],
pub u: f32,
pub v: f32,
}
#[allow(dead_code)]
pub fn default_background_config() -> BackgroundConfig {
BackgroundConfig {
bg_type: BackgroundType::SolidColor,
top_color: [0.15, 0.15, 0.18, 1.0],
bottom_color: [0.05, 0.05, 0.07, 1.0],
checker_size: 16,
}
}
#[allow(dead_code)]
pub fn new_background_config(bg_type: BackgroundType) -> BackgroundConfig {
BackgroundConfig {
bg_type,
..default_background_config()
}
}
#[allow(dead_code)]
pub fn sample_background(config: &BackgroundConfig, u: f32, v: f32) -> BackgroundSample {
let color = match config.bg_type {
BackgroundType::SolidColor => config.top_color,
BackgroundType::VerticalGradient => {
gradient_color(&config.top_color, &config.bottom_color, v)
}
BackgroundType::HorizontalGradient => {
gradient_color(&config.top_color, &config.bottom_color, u)
}
BackgroundType::Checkerboard => checkerboard_color(config, u, v),
BackgroundType::Cubemap => sample_cubemap(config, u, v),
};
BackgroundSample { color, u, v }
}
#[allow(dead_code)]
pub fn gradient_color(a: &[f32; 4], b: &[f32; 4], t: f32) -> [f32; 4] {
let t = t.clamp(0.0, 1.0);
[
a[0] + (b[0] - a[0]) * t,
a[1] + (b[1] - a[1]) * t,
a[2] + (b[2] - a[2]) * t,
a[3] + (b[3] - a[3]) * t,
]
}
#[allow(dead_code)]
pub fn checkerboard_color(config: &BackgroundConfig, u: f32, v: f32) -> [f32; 4] {
let size = config.checker_size.max(1) as f32;
let px = (u * 1024.0) / size;
let py = (v * 1024.0) / size;
let checker = ((px as i32) + (py as i32)) % 2 == 0;
if checker {
config.top_color
} else {
config.bottom_color
}
}
fn uv_to_direction(u: f32, v: f32) -> [f32; 3] {
let az = u * std::f32::consts::TAU;
let el = (0.5 - v) * std::f32::consts::PI; let ce = el.cos();
[ce * az.sin(), el.sin(), ce * az.cos()]
}
#[allow(dead_code)]
fn direction_to_face_uv(d: [f32; 3]) -> (usize, f32, f32) {
let ax = d[0].abs();
let ay = d[1].abs();
let az = d[2].abs();
let (face, sc, tc, ma) = if ax >= ay && ax >= az {
if d[0] > 0.0 {
(0usize, -d[2], -d[1], ax)
} else {
(1, d[2], -d[1], ax)
}
} else if ay >= az {
if d[1] > 0.0 {
(2, d[0], d[2], ay)
} else {
(3, d[0], -d[2], ay)
}
} else if d[2] > 0.0 {
(4, d[0], -d[1], az)
} else {
(5, -d[0], -d[1], az)
};
let inv = if ma.abs() < 1e-12 { 0.0 } else { 1.0 / ma };
let s = 0.5 * (sc * inv + 1.0);
let t = 0.5 * (tc * inv + 1.0);
(face, s.clamp(0.0, 1.0), t.clamp(0.0, 1.0))
}
#[allow(dead_code)]
pub fn sample_cubemap(config: &BackgroundConfig, u: f32, v: f32) -> [f32; 4] {
let d = uv_to_direction(u, v);
let elev = d[1].clamp(-1.0, 1.0);
let mut tt = 0.5 * (elev + 1.0);
tt = tt * tt * (3.0 - 2.0 * tt);
let mut col = gradient_color(&config.bottom_color, &config.top_color, tt);
let haze = (1.0 - (elev.abs() * 3.0).min(1.0)) * 0.12;
for c in col.iter_mut().take(3) {
*c = (*c + haze).clamp(0.0, 1.0);
}
let sun_dir = {
let s = [0.3f32, 0.6, 0.2];
let l = (s[0] * s[0] + s[1] * s[1] + s[2] * s[2]).sqrt();
[s[0] / l, s[1] / l, s[2] / l]
};
let ndl = (d[0] * sun_dir[0] + d[1] * sun_dir[1] + d[2] * sun_dir[2]).max(0.0);
let sun = ndl.powf(64.0) * 0.8;
for c in col.iter_mut().take(3) {
*c = (*c + sun).clamp(0.0, 1.0);
}
col[3] = config.top_color[3];
col
}
#[allow(dead_code)]
pub fn set_background_type(config: &mut BackgroundConfig, bg_type: BackgroundType) {
config.bg_type = bg_type;
}
#[allow(dead_code)]
pub fn set_top_color(config: &mut BackgroundConfig, color: [f32; 4]) {
config.top_color = color;
}
#[allow(dead_code)]
pub fn set_bottom_color(config: &mut BackgroundConfig, color: [f32; 4]) {
config.bottom_color = color;
}
#[allow(dead_code)]
pub fn set_checker_size(config: &mut BackgroundConfig, size: u32) {
config.checker_size = size.max(1);
}
#[allow(dead_code)]
pub fn background_type_name(bg_type: BackgroundType) -> &'static str {
match bg_type {
BackgroundType::SolidColor => "Solid Color",
BackgroundType::VerticalGradient => "Vertical Gradient",
BackgroundType::HorizontalGradient => "Horizontal Gradient",
BackgroundType::Checkerboard => "Checkerboard",
BackgroundType::Cubemap => "Cubemap",
}
}
#[allow(dead_code)]
pub fn is_dynamic_background(bg_type: BackgroundType) -> bool {
!matches!(bg_type, BackgroundType::SolidColor)
}
#[allow(dead_code)]
pub fn background_to_json(config: &BackgroundConfig) -> String {
format!(
concat!(
"{{\n",
" \"type\": \"{}\",\n",
" \"top_color\": [{:.6}, {:.6}, {:.6}, {:.6}],\n",
" \"bottom_color\": [{:.6}, {:.6}, {:.6}, {:.6}],\n",
" \"checker_size\": {}\n",
"}}"
),
background_type_name(config.bg_type),
config.top_color[0],
config.top_color[1],
config.top_color[2],
config.top_color[3],
config.bottom_color[0],
config.bottom_color[1],
config.bottom_color[2],
config.bottom_color[3],
config.checker_size,
)
}
#[allow(dead_code)]
pub fn background_pixel_grid(
config: &BackgroundConfig,
width: u32,
height: u32,
) -> Vec<BackgroundSample> {
let w = width.max(1);
let h = height.max(1);
let mut out = Vec::with_capacity((w * h) as usize);
for row in 0..h {
let v = if h > 1 {
row as f32 / (h - 1) as f32
} else {
0.0
};
for col in 0..w {
let u = if w > 1 {
col as f32 / (w - 1) as f32
} else {
0.0
};
out.push(sample_background(config, u, v));
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_is_solid() {
let cfg = default_background_config();
assert_eq!(cfg.bg_type, BackgroundType::SolidColor);
}
#[test]
fn new_background_config_sets_type() {
let cfg = new_background_config(BackgroundType::Checkerboard);
assert_eq!(cfg.bg_type, BackgroundType::Checkerboard);
}
#[test]
fn sample_solid_returns_top_color() {
let cfg = default_background_config();
let s = sample_background(&cfg, 0.5, 0.5);
assert_eq!(s.color, cfg.top_color);
}
#[test]
fn gradient_color_at_zero() {
let a = [1.0, 0.0, 0.0, 1.0];
let b = [0.0, 1.0, 0.0, 1.0];
let c = gradient_color(&a, &b, 0.0);
assert!((c[0] - 1.0).abs() < 1e-6);
assert!((c[1] - 0.0).abs() < 1e-6);
}
#[test]
fn gradient_color_at_one() {
let a = [1.0, 0.0, 0.0, 1.0];
let b = [0.0, 1.0, 0.0, 1.0];
let c = gradient_color(&a, &b, 1.0);
assert!((c[0] - 0.0).abs() < 1e-6);
assert!((c[1] - 1.0).abs() < 1e-6);
}
#[test]
fn gradient_color_midpoint() {
let a = [0.0, 0.0, 0.0, 1.0];
let b = [1.0, 1.0, 1.0, 1.0];
let c = gradient_color(&a, &b, 0.5);
assert!((c[0] - 0.5).abs() < 1e-6);
}
#[test]
fn vertical_gradient_top_is_top_color() {
let mut cfg = default_background_config();
set_background_type(&mut cfg, BackgroundType::VerticalGradient);
set_top_color(&mut cfg, [1.0, 0.0, 0.0, 1.0]);
set_bottom_color(&mut cfg, [0.0, 0.0, 1.0, 1.0]);
let s = sample_background(&cfg, 0.5, 0.0);
assert!((s.color[0] - 1.0).abs() < 1e-6);
}
#[test]
fn horizontal_gradient_varies_by_u() {
let mut cfg = new_background_config(BackgroundType::HorizontalGradient);
cfg.top_color = [0.0, 0.0, 0.0, 1.0];
cfg.bottom_color = [1.0, 1.0, 1.0, 1.0];
let left = sample_background(&cfg, 0.0, 0.5);
let right = sample_background(&cfg, 1.0, 0.5);
assert!((left.color[0] - 0.0).abs() < 1e-6);
assert!((right.color[0] - 1.0).abs() < 1e-6);
}
#[test]
fn checkerboard_alternates() {
let mut cfg = new_background_config(BackgroundType::Checkerboard);
cfg.top_color = [1.0, 1.0, 1.0, 1.0];
cfg.bottom_color = [0.0, 0.0, 0.0, 1.0];
cfg.checker_size = 16;
let s1 = sample_background(&cfg, 0.0, 0.0);
let s2 = sample_background(&cfg, 16.0 / 1024.0, 0.0);
assert_ne!(s1.color, s2.color);
}
#[test]
fn set_checker_size_clamps_min() {
let mut cfg = default_background_config();
set_checker_size(&mut cfg, 0);
assert_eq!(cfg.checker_size, 1);
}
#[test]
fn background_type_name_all_variants() {
assert_eq!(
background_type_name(BackgroundType::SolidColor),
"Solid Color"
);
assert_eq!(
background_type_name(BackgroundType::VerticalGradient),
"Vertical Gradient"
);
assert_eq!(
background_type_name(BackgroundType::HorizontalGradient),
"Horizontal Gradient"
);
assert_eq!(
background_type_name(BackgroundType::Checkerboard),
"Checkerboard"
);
assert_eq!(background_type_name(BackgroundType::Cubemap), "Cubemap");
}
#[test]
fn is_dynamic_background_correct() {
assert!(!is_dynamic_background(BackgroundType::SolidColor));
assert!(is_dynamic_background(BackgroundType::VerticalGradient));
assert!(is_dynamic_background(BackgroundType::HorizontalGradient));
assert!(is_dynamic_background(BackgroundType::Checkerboard));
assert!(is_dynamic_background(BackgroundType::Cubemap));
}
#[test]
fn background_to_json_contains_type() {
let cfg = default_background_config();
let json = background_to_json(&cfg);
assert!(json.contains("Solid Color"));
assert!(json.contains("top_color"));
assert!(json.contains("bottom_color"));
}
#[test]
fn background_pixel_grid_correct_count() {
let cfg = default_background_config();
let grid = background_pixel_grid(&cfg, 4, 3);
assert_eq!(grid.len(), 12);
}
#[test]
fn background_pixel_grid_single_pixel() {
let cfg = default_background_config();
let grid = background_pixel_grid(&cfg, 1, 1);
assert_eq!(grid.len(), 1);
}
#[test]
fn cubemap_is_direction_dependent() {
let cfg = new_background_config(BackgroundType::Cubemap);
let up = sample_background(&cfg, 0.5, 0.0); let down = sample_background(&cfg, 0.5, 1.0); assert_ne!(up.color, down.color);
assert!((up.color[2] - cfg.top_color[2]).abs() < 0.06);
assert!((down.color[2] - cfg.bottom_color[2]).abs() < 0.06);
}
#[test]
fn direction_to_face_uv_axes() {
let (face, s, t) = direction_to_face_uv([1.0, 0.0, 0.0]);
assert_eq!(face, 0);
assert!((s - 0.5).abs() < 1e-6 && (t - 0.5).abs() < 1e-6);
assert_eq!(direction_to_face_uv([0.0, 1.0, 0.0]).0, 2);
assert_eq!(direction_to_face_uv([0.0, 0.0, -1.0]).0, 5);
}
}