use crate::{
Color, PixelSize, Point, Time, ValidationError,
noise::{self, Fractal},
};
#[cfg(test)]
mod tests;
const MAX_CANVAS_AXIS: u32 = 16384;
const MIN_SHADER_ENVELOPE: f64 = 1e-15;
const OCTAVE_WORK: u64 = 192;
const ENVELOPE_WORK: u64 = 256;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum AtmosphereKind {
Fog {
gradient: [f64; 2],
},
Dust {
radius: f64,
coverage: f64,
},
LightLeak {
center: [f64; 2],
extent: [f64; 2],
},
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AtmosphereParameters {
pub kind: AtmosphereKind,
pub seed: u32,
pub color: Color,
pub opacity: f64,
pub intensity: f64,
pub scale: f64,
pub drift: [f64; 2],
pub fractal: Fractal,
}
impl AtmosphereParameters {
pub fn validate(&self) -> Result<(), ValidationError> {
self.fractal.validate()?;
let bounded = |value: f64, min, max| value.is_finite() && (min..=max).contains(&value);
let valid_kind = match self.kind {
AtmosphereKind::Fog { gradient } => {
gradient.into_iter().all(|value| bounded(value, 0.0, 8.0))
}
AtmosphereKind::Dust { radius, coverage } => {
radius.is_finite()
&& radius > 0.0
&& radius <= self.scale * 0.5
&& bounded(coverage, 0.0, 1.0)
}
AtmosphereKind::LightLeak { center, extent } => {
center.into_iter().all(|value| bounded(value, -1.0, 2.0))
&& extent
.into_iter()
.all(|value| value.is_finite() && value > 0.0 && value <= 4.0)
}
};
if !valid_kind
|| !bounded(self.opacity, 0.0, 1.0)
|| !bounded(self.intensity, 0.0, 64.0)
|| !bounded(self.scale, 0.25, 1e6)
|| self.drift.into_iter().any(|value| !value.is_finite())
|| self.color.to_linear_premultiplied().iter().any(|value| {
!(*value * self.intensity).is_finite()
|| !((*value * self.intensity) as f32).is_finite()
})
{
return Err(ValidationError::invalid(
"atmosphere",
"parameters",
"Require finite bounded envelope, color, opacity, intensity, scale and drift",
));
}
Ok(())
}
pub fn sample(
&self,
pixel: [f64; 2],
canvas: PixelSize,
phase: [f64; 2],
) -> Result<[f64; 4], ValidationError> {
self.validate()?;
if canvas.width == 0
|| canvas.height == 0
|| pixel
.into_iter()
.chain(phase)
.any(|value| !value.is_finite())
{
return Err(ValidationError::invalid(
"atmosphere",
"point",
"Atmosphere coordinates must be finite with a positive canvas",
));
}
let point = [
(pixel[0] + phase[0]) / self.scale,
(pixel[1] + phase[1]) / self.scale,
];
let cloud = (0.5
+ 0.5
* noise::fbm(
|point| noise::perlin2(point, self.seed),
point,
self.fractal,
)?)
.clamp(0.0, 1.0);
let envelope = match self.kind {
AtmosphereKind::Fog { gradient } => {
let vertical = pixel[1] / f64::from(canvas.height);
let density = gradient[0] * (1.0 - vertical) + gradient[1] * vertical;
1.0 - libm::exp(-self.opacity * density * (0.35 + cloud))
}
AtmosphereKind::Dust { radius, coverage } => {
self.opacity
* dust(point, self.seed, radius / self.scale, coverage)
* (0.7 + 0.3 * cloud)
}
AtmosphereKind::LightLeak { center, extent } => {
let horizontal = (pixel[0] / f64::from(canvas.width) - center[0]) / extent[0];
let vertical = (pixel[1] / f64::from(canvas.height) - center[1]) / extent[1];
self.opacity
* libm::exp(-2.0 * (horizontal * horizontal + vertical * vertical))
* (0.6 + 0.4 * cloud)
}
};
let tint = self.color.to_linear_premultiplied();
let alpha = tint[3];
let rgb = [tint[0], tint[1], tint[2]].map(|value| {
if alpha > 0.0 {
value / alpha * self.intensity
} else {
0.0
}
});
Ok([rgb[0], rgb[1], rgb[2], (envelope * alpha).clamp(0.0, 1.0)])
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AtmospherePaint {
parameters: AtmosphereParameters,
canvas: PixelSize,
time: Time,
}
impl AtmospherePaint {
pub fn new(
parameters: AtmosphereParameters,
canvas: PixelSize,
time: Time,
) -> Result<Self, ValidationError> {
let paint = Self {
parameters,
canvas,
time,
};
paint.validate()?;
Ok(paint)
}
pub fn parameters(&self) -> &AtmosphereParameters {
&self.parameters
}
pub fn canvas(&self) -> PixelSize {
self.canvas
}
pub fn time(&self) -> Time {
self.time
}
pub fn phase(&self) -> [f64; 2] {
self.parameters
.drift
.map(|velocity| -velocity * self.time.as_seconds())
}
pub fn validate(&self) -> Result<(), ValidationError> {
self.parameters.validate()?;
let supported_envelope = match self.parameters.kind {
AtmosphereKind::Fog { .. } => true,
AtmosphereKind::Dust { radius, .. } => {
radius / self.parameters.scale >= MIN_SHADER_ENVELOPE
}
AtmosphereKind::LightLeak { extent, .. } => {
extent.into_iter().all(|value| value >= MIN_SHADER_ENVELOPE)
}
};
if !supported_envelope {
return Err(ValidationError::invalid(
"atmosphere",
"envelope",
"Atmosphere envelope is too small for finite shader arithmetic",
));
}
if self.canvas.width == 0
|| self.canvas.height == 0
|| self.canvas.width > MAX_CANVAS_AXIS
|| self.canvas.height > MAX_CANVAS_AXIS
{
return Err(ValidationError::invalid(
"atmosphere",
"canvas",
"Canvas axes must be in 1..=16384",
));
}
let phase = self.phase();
for horizontal in [0.0, f64::from(self.canvas.width)] {
for vertical in [0.0, f64::from(self.canvas.height)] {
let point = [
(horizontal + phase[0]) / self.parameters.scale,
(vertical + phase[1]) / self.parameters.scale,
];
noise::fbm(|_| 0.0, point, self.parameters.fractal)?;
let mut narrowed = [
(horizontal as f32 + phase[0] as f32) / self.parameters.scale as f32,
(vertical as f32 + phase[1] as f32) / self.parameters.scale as f32,
];
for _ in 0..self.parameters.fractal.octaves {
if narrowed.iter().any(|value| {
!value.is_finite() || *value < i32::MIN as f32 || *value >= i32::MAX as f32
}) {
return Err(ValidationError::invalid(
"atmosphere",
"point",
"Atmosphere exceeds GPU lattice support",
));
}
narrowed =
narrowed.map(|value| value * self.parameters.fractal.lacunarity as f32);
}
}
}
Ok(())
}
pub fn sample(&self, point: Point) -> Result<[f32; 4], ValidationError> {
let pixel = [
point.x * f64::from(self.canvas.width),
point.y * f64::from(self.canvas.height),
];
let value = self.parameters.sample(pixel, self.canvas, self.phase())?;
Ok([
value[0] * value[3],
value[1] * value[3],
value[2] * value[3],
value[3],
]
.map(|value| value as f32))
}
pub fn work_per_pixel(&self) -> u64 {
u64::from(self.parameters.fractal.octaves) * OCTAVE_WORK + ENVELOPE_WORK
}
}
impl From<AtmospherePaint> for super::Paint {
fn from(value: AtmospherePaint) -> Self {
Self::Atmosphere(value)
}
}
fn dust(point: [f64; 2], seed: u32, radius: f64, coverage: f64) -> f64 {
let cell = point.map(|value| value.floor() as i32);
let unit = |value: u32| f64::from(value) / f64::from(u32::MAX);
let mut result: f64 = 0.0;
for dy in -1i32..=1 {
for dx in -1i32..=1 {
let horizontal = cell[0].wrapping_add(dx);
let vertical = cell[1].wrapping_add(dy);
let hash = noise::hash2(horizontal, vertical, seed);
if unit(hash) >= coverage {
continue;
}
let center = [
f64::from(horizontal) + unit(noise::hash32(hash ^ 0xa511_e9b3)),
f64::from(vertical) + unit(noise::hash32(hash ^ 0x63d8_3595)),
];
let radius = radius * (0.6 + 0.4 * unit(noise::hash32(hash ^ 0x9e37_79b9)));
let distance = (point[0] - center[0]).powi(2) + (point[1] - center[1]).powi(2);
result = result.max((1.0 - distance / (radius * radius)).max(0.0).powi(2));
}
}
result
}