1use crate::{
4 Color, PixelSize, Point, Time, ValidationError,
5 noise::{self, Fractal},
6};
7
8#[cfg(test)]
9mod tests;
10
11const MAX_CANVAS_AXIS: u32 = 16384;
13
14const MIN_SHADER_ENVELOPE: f64 = 1e-15;
16
17const OCTAVE_WORK: u64 = 192;
19
20const ENVELOPE_WORK: u64 = 256;
22
23#[derive(Clone, Copy, Debug, PartialEq)]
25pub enum AtmosphereKind {
26 Fog {
28 gradient: [f64; 2],
30 },
31 Dust {
33 radius: f64,
35 coverage: f64,
37 },
38 LightLeak {
40 center: [f64; 2],
42 extent: [f64; 2],
44 },
45}
46
47#[derive(Clone, Copy, Debug, PartialEq)]
49pub struct AtmosphereParameters {
50 pub kind: AtmosphereKind,
52 pub seed: u32,
54 pub color: Color,
56 pub opacity: f64,
58 pub intensity: f64,
60 pub scale: f64,
62 pub drift: [f64; 2],
64 pub fractal: Fractal,
66}
67
68impl AtmosphereParameters {
69 pub fn validate(&self) -> Result<(), ValidationError> {
71 self.fractal.validate()?;
72 let bounded = |value: f64, min, max| value.is_finite() && (min..=max).contains(&value);
73 let valid_kind = match self.kind {
74 AtmosphereKind::Fog { gradient } => {
75 gradient.into_iter().all(|value| bounded(value, 0.0, 8.0))
76 }
77 AtmosphereKind::Dust { radius, coverage } => {
78 radius.is_finite()
79 && radius > 0.0
80 && radius <= self.scale * 0.5
81 && bounded(coverage, 0.0, 1.0)
82 }
83 AtmosphereKind::LightLeak { center, extent } => {
84 center.into_iter().all(|value| bounded(value, -1.0, 2.0))
85 && extent
86 .into_iter()
87 .all(|value| value.is_finite() && value > 0.0 && value <= 4.0)
88 }
89 };
90 if !valid_kind
91 || !bounded(self.opacity, 0.0, 1.0)
92 || !bounded(self.intensity, 0.0, 64.0)
93 || !bounded(self.scale, 0.25, 1e6)
94 || self.drift.into_iter().any(|value| !value.is_finite())
95 || self.color.to_linear_premultiplied().iter().any(|value| {
96 !(*value * self.intensity).is_finite()
97 || !((*value * self.intensity) as f32).is_finite()
98 })
99 {
100 return Err(ValidationError::invalid(
101 "atmosphere",
102 "parameters",
103 "Require finite bounded envelope, color, opacity, intensity, scale and drift",
104 ));
105 }
106 Ok(())
107 }
108
109 pub fn sample(
114 &self,
115 pixel: [f64; 2],
116 canvas: PixelSize,
117 phase: [f64; 2],
118 ) -> Result<[f64; 4], ValidationError> {
119 self.validate()?;
120 if canvas.width == 0
121 || canvas.height == 0
122 || pixel
123 .into_iter()
124 .chain(phase)
125 .any(|value| !value.is_finite())
126 {
127 return Err(ValidationError::invalid(
128 "atmosphere",
129 "point",
130 "Atmosphere coordinates must be finite with a positive canvas",
131 ));
132 }
133 let point = [
134 (pixel[0] + phase[0]) / self.scale,
135 (pixel[1] + phase[1]) / self.scale,
136 ];
137 let cloud = (0.5
138 + 0.5
139 * noise::fbm(
140 |point| noise::perlin2(point, self.seed),
141 point,
142 self.fractal,
143 )?)
144 .clamp(0.0, 1.0);
145 let envelope = match self.kind {
146 AtmosphereKind::Fog { gradient } => {
147 let vertical = pixel[1] / f64::from(canvas.height);
148 let density = gradient[0] * (1.0 - vertical) + gradient[1] * vertical;
149 1.0 - libm::exp(-self.opacity * density * (0.35 + cloud))
150 }
151 AtmosphereKind::Dust { radius, coverage } => {
152 self.opacity
153 * dust(point, self.seed, radius / self.scale, coverage)
154 * (0.7 + 0.3 * cloud)
155 }
156 AtmosphereKind::LightLeak { center, extent } => {
157 let horizontal = (pixel[0] / f64::from(canvas.width) - center[0]) / extent[0];
158 let vertical = (pixel[1] / f64::from(canvas.height) - center[1]) / extent[1];
159 self.opacity
160 * libm::exp(-2.0 * (horizontal * horizontal + vertical * vertical))
161 * (0.6 + 0.4 * cloud)
162 }
163 };
164 let tint = self.color.to_linear_premultiplied();
165 let alpha = tint[3];
166 let rgb = [tint[0], tint[1], tint[2]].map(|value| {
167 if alpha > 0.0 {
168 value / alpha * self.intensity
169 } else {
170 0.0
171 }
172 });
173 Ok([rgb[0], rgb[1], rgb[2], (envelope * alpha).clamp(0.0, 1.0)])
174 }
175}
176
177#[derive(Clone, Copy, Debug, PartialEq)]
179pub struct AtmospherePaint {
180 parameters: AtmosphereParameters,
181 canvas: PixelSize,
182 time: Time,
183}
184
185impl AtmospherePaint {
186 pub fn new(
188 parameters: AtmosphereParameters,
189 canvas: PixelSize,
190 time: Time,
191 ) -> Result<Self, ValidationError> {
192 let paint = Self {
193 parameters,
194 canvas,
195 time,
196 };
197 paint.validate()?;
198 Ok(paint)
199 }
200
201 pub fn parameters(&self) -> &AtmosphereParameters {
203 &self.parameters
204 }
205
206 pub fn canvas(&self) -> PixelSize {
208 self.canvas
209 }
210
211 pub fn time(&self) -> Time {
213 self.time
214 }
215
216 pub fn phase(&self) -> [f64; 2] {
218 self.parameters
219 .drift
220 .map(|velocity| -velocity * self.time.as_seconds())
221 }
222
223 pub fn validate(&self) -> Result<(), ValidationError> {
225 self.parameters.validate()?;
226 let supported_envelope = match self.parameters.kind {
227 AtmosphereKind::Fog { .. } => true,
228 AtmosphereKind::Dust { radius, .. } => {
229 radius / self.parameters.scale >= MIN_SHADER_ENVELOPE
230 }
231 AtmosphereKind::LightLeak { extent, .. } => {
232 extent.into_iter().all(|value| value >= MIN_SHADER_ENVELOPE)
233 }
234 };
235 if !supported_envelope {
236 return Err(ValidationError::invalid(
237 "atmosphere",
238 "envelope",
239 "Atmosphere envelope is too small for finite shader arithmetic",
240 ));
241 }
242 if self.canvas.width == 0
243 || self.canvas.height == 0
244 || self.canvas.width > MAX_CANVAS_AXIS
245 || self.canvas.height > MAX_CANVAS_AXIS
246 {
247 return Err(ValidationError::invalid(
248 "atmosphere",
249 "canvas",
250 "Canvas axes must be in 1..=16384",
251 ));
252 }
253 let phase = self.phase();
254 for horizontal in [0.0, f64::from(self.canvas.width)] {
255 for vertical in [0.0, f64::from(self.canvas.height)] {
256 let point = [
257 (horizontal + phase[0]) / self.parameters.scale,
258 (vertical + phase[1]) / self.parameters.scale,
259 ];
260 noise::fbm(|_| 0.0, point, self.parameters.fractal)?;
261 let mut narrowed = [
262 (horizontal as f32 + phase[0] as f32) / self.parameters.scale as f32,
263 (vertical as f32 + phase[1] as f32) / self.parameters.scale as f32,
264 ];
265 for _ in 0..self.parameters.fractal.octaves {
266 if narrowed.iter().any(|value| {
267 !value.is_finite() || *value < i32::MIN as f32 || *value >= i32::MAX as f32
268 }) {
269 return Err(ValidationError::invalid(
270 "atmosphere",
271 "point",
272 "Atmosphere exceeds GPU lattice support",
273 ));
274 }
275 narrowed =
276 narrowed.map(|value| value * self.parameters.fractal.lacunarity as f32);
277 }
278 }
279 }
280 Ok(())
281 }
282
283 pub fn sample(&self, point: Point) -> Result<[f32; 4], ValidationError> {
285 let pixel = [
286 point.x * f64::from(self.canvas.width),
287 point.y * f64::from(self.canvas.height),
288 ];
289 let value = self.parameters.sample(pixel, self.canvas, self.phase())?;
290 Ok([
291 value[0] * value[3],
292 value[1] * value[3],
293 value[2] * value[3],
294 value[3],
295 ]
296 .map(|value| value as f32))
297 }
298
299 pub fn work_per_pixel(&self) -> u64 {
301 u64::from(self.parameters.fractal.octaves) * OCTAVE_WORK + ENVELOPE_WORK
302 }
303}
304
305impl From<AtmospherePaint> for super::Paint {
306 fn from(value: AtmospherePaint) -> Self {
307 Self::Atmosphere(value)
308 }
309}
310
311fn dust(point: [f64; 2], seed: u32, radius: f64, coverage: f64) -> f64 {
312 let cell = point.map(|value| value.floor() as i32);
313 let unit = |value: u32| f64::from(value) / f64::from(u32::MAX);
314 let mut result: f64 = 0.0;
315 for dy in -1i32..=1 {
316 for dx in -1i32..=1 {
317 let horizontal = cell[0].wrapping_add(dx);
318 let vertical = cell[1].wrapping_add(dy);
319 let hash = noise::hash2(horizontal, vertical, seed);
320 if unit(hash) >= coverage {
321 continue;
322 }
323 let center = [
324 f64::from(horizontal) + unit(noise::hash32(hash ^ 0xa511_e9b3)),
325 f64::from(vertical) + unit(noise::hash32(hash ^ 0x63d8_3595)),
326 ];
327 let radius = radius * (0.6 + 0.4 * unit(noise::hash32(hash ^ 0x9e37_79b9)));
328 let distance = (point[0] - center[0]).powi(2) + (point[1] - center[1]).powi(2);
329 result = result.max((1.0 - distance / (radius * radius)).max(0.0).powi(2));
330 }
331 }
332 result
333}