use std::f32::consts::{FRAC_PI_2, PI};
use embed_doc_image::embed_doc_image;
use thiserror::Error;
pub enum Channel {
R = 0,
G = 1,
B = 2,
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct SkyParams {
pub elevation: f32,
pub turbidity: f32,
pub albedo: [f32; 3],
}
impl Default for SkyParams {
fn default() -> Self {
Self {
elevation: 0.0,
turbidity: 1.0,
albedo: [1.0, 1.0, 1.0],
}
}
}
#[derive(Error, PartialEq, Debug)]
pub enum Error {
#[error("Elevation must be between 0..=π/2, got {0} instead")]
ElevationOutOfRange(f32),
#[error("Turbidity must be between 1..=10, got {0} instead")]
TurbidityOutOfRange(f32),
#[error("Albedo must be between 0..=1, got {0:?} instead")]
AlbedoOutOfRange([f32; 3]),
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct SkyState {
params: [f32; 27],
radiances: [f32; 3],
}
impl SkyState {
pub fn new(sky_params: &SkyParams) -> Result<Self, Error> {
if !(0.0..=FRAC_PI_2).contains(&sky_params.elevation) {
return Err(Error::ElevationOutOfRange(sky_params.elevation));
}
if !(1.0..=10.0).contains(&sky_params.turbidity) {
return Err(Error::TurbidityOutOfRange(sky_params.turbidity));
}
for albedo in sky_params.albedo {
if !(0.0..=1.0).contains(&albedo) {
return Err(Error::AlbedoOutOfRange(sky_params.albedo));
}
}
macro_rules! dataset {
($path: literal) => {{
use include_bytes_aligned::include_bytes_aligned;
let bytes = include_bytes_aligned!(4, $path);
bytemuck::cast_slice::<_, f32>(bytes)
}};
}
let params_r = dataset!("params-r");
let params_g = dataset!("params-g");
let params_b = dataset!("params-b");
let radiances_r = dataset!("radiances-r");
let radiances_g = dataset!("radiances-g");
let radiances_b = dataset!("radiances-b");
let mut params = [0.0; 27];
let mut radiances = [0.0; 3];
let elevation = sky_params.elevation;
let turbidity = sky_params.turbidity;
let albedo = sky_params.albedo;
let t = (elevation / (0.5 * PI)).powf(1.0 / 3.0);
init_params(&mut params[..], params_r, turbidity, albedo[0], t);
init_params(&mut params[9..], params_g, turbidity, albedo[1], t);
init_params(&mut params[(9 * 2)..], params_b, turbidity, albedo[2], t);
init_radiances(&mut radiances[0], radiances_r, turbidity, albedo[0], t);
init_radiances(&mut radiances[1], radiances_g, turbidity, albedo[1], t);
init_radiances(&mut radiances[2], radiances_b, turbidity, albedo[2], t);
Ok(Self { params, radiances })
}
#[embed_doc_image("coordinate-system", "images/coordinate-system.png")]
pub fn radiance(&self, theta: f32, gamma: f32, channel: Channel) -> f32 {
let channel = channel as usize;
let r = self.radiances[channel];
let p = &self.params[(9 * channel)..];
let p0 = p[0];
let p1 = p[1];
let p2 = p[2];
let p3 = p[3];
let p4 = p[4];
let p5 = p[5];
let p6 = p[6];
let p7 = p[7];
let p8 = p[8];
let cos_gamma = gamma.cos();
let cos_gamma2 = cos_gamma * cos_gamma;
let cos_theta = theta.cos().abs();
let exp_m = (p4 * gamma).exp();
let ray_m = cos_gamma2;
let mie_m_lhs = 1.0 + cos_gamma2;
let mie_m_rhs = (1.0 + p8 * p8 - 2.0 * p8 * cos_gamma).powf(1.5);
let mie_m = mie_m_lhs / mie_m_rhs;
let zenith = cos_theta.sqrt();
let radiance_lhs = 1.0 + p0 * (p1 / (cos_theta + 0.01)).exp();
let radiance_rhs = p2 + p3 * exp_m + p5 * ray_m + p6 * mie_m + p7 * zenith;
let radiance_dist = radiance_lhs * radiance_rhs;
r * radiance_dist
}
pub fn raw(&self) -> ([f32; 27], [f32; 3]) {
(self.params, self.radiances)
}
}
fn init_params(out_params: &mut [f32], dataset: &[f32], turbidity: f32, albedo: f32, t: f32) {
let turbidity_int = turbidity.trunc() as usize;
let turbidity_rem = turbidity.fract();
let turbidity_min = turbidity_int.saturating_sub(1);
let turbidity_max = turbidity_int.min(9);
let p0 = &dataset[(9 * 6 * turbidity_min)..];
let p1 = &dataset[(9 * 6 * turbidity_max)..];
let p2 = &dataset[(9 * 6 * 10 + 9 * 6 * turbidity_min)..];
let p3 = &dataset[(9 * 6 * 10 + 9 * 6 * turbidity_max)..];
let s0 = (1.0 - albedo) * (1.0 - turbidity_rem);
let s1 = (1.0 - albedo) * turbidity_rem;
let s2 = albedo * (1.0 - turbidity_rem);
let s3 = albedo * turbidity_rem;
for i in 0..9 {
out_params[i] += s0 * quintic::<9>(&p0[i..], t);
out_params[i] += s1 * quintic::<9>(&p1[i..], t);
out_params[i] += s2 * quintic::<9>(&p2[i..], t);
out_params[i] += s3 * quintic::<9>(&p3[i..], t);
}
}
fn init_radiances(out_radiance: &mut f32, dataset: &[f32], turbidity: f32, albedo: f32, t: f32) {
let turbidity_int = turbidity.trunc() as usize;
let turbidity_rem = turbidity.fract();
let turbidity_min = turbidity_int.saturating_sub(1);
let turbidity_max = turbidity_int.min(9);
let p0 = &dataset[(6 * turbidity_min)..];
let p1 = &dataset[(6 * turbidity_max)..];
let p2 = &dataset[(6 * 10 + 6 * turbidity_min)..];
let p3 = &dataset[(6 * 10 + 6 * turbidity_max)..];
let s0 = (1.0 - albedo) * (1.0 - turbidity_rem);
let s1 = (1.0 - albedo) * turbidity_rem;
let s2 = albedo * (1.0 - turbidity_rem);
let s3 = albedo * turbidity_rem;
*out_radiance += s0 * quintic::<1>(p0, t);
*out_radiance += s1 * quintic::<1>(p1, t);
*out_radiance += s2 * quintic::<1>(p2, t);
*out_radiance += s3 * quintic::<1>(p3, t);
}
fn quintic<const STRIDE: usize>(p: &[f32], t: f32) -> f32 {
let t2 = t * t;
let t3 = t2 * t;
let t4 = t2 * t2;
let t5 = t4 * t;
let inv_t = 1.0 - t;
let inv_t2 = inv_t * inv_t;
let inv_t3 = inv_t2 * inv_t;
let inv_t4 = inv_t2 * inv_t2;
let inv_t5 = inv_t4 * inv_t;
let m0 = p[0] * inv_t5;
let m1 = p[STRIDE] * 5.0 * inv_t4 * t;
let m2 = p[2 * STRIDE] * 10.0 * inv_t3 * t2;
let m3 = p[3 * STRIDE] * 10.0 * inv_t2 * t3;
let m4 = p[4 * STRIDE] * 5.0 * inv_t * t4;
let m5 = p[5 * STRIDE] * t5;
m0 + m1 + m2 + m3 + m4 + m5
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default() {
let state = SkyState::new(&SkyParams::default());
assert!(state.is_ok());
}
#[test]
fn elevation_out_of_range() {
let params = SkyParams {
elevation: -1.0,
..SkyParams::default()
};
let state = SkyState::new(¶ms);
assert_eq!(state, Err(Error::ElevationOutOfRange(-1.0)));
assert_eq!(
"Elevation must be between 0..=π/2, got -1 instead",
format!("{}", state.unwrap_err())
);
}
#[test]
fn turbidity_out_of_range() {
let params = SkyParams {
turbidity: 0.0,
..SkyParams::default()
};
let state = SkyState::new(¶ms);
assert_eq!(state, Err(Error::TurbidityOutOfRange(0.0)));
assert_eq!(
"Turbidity must be between 1..=10, got 0 instead",
format!("{}", state.unwrap_err())
);
}
#[test]
fn albedo_out_of_range() {
let params = SkyParams {
albedo: [0.0, 2.0, 0.0],
..SkyParams::default()
};
let state = SkyState::new(¶ms);
assert_eq!(state, Err(Error::AlbedoOutOfRange([0.0, 2.0, 0.0])));
assert_eq!(
"Albedo must be between 0..=1, got [0.0, 2.0, 0.0] instead",
format!("{}", state.unwrap_err())
);
}
}