Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_light/src/atmosphere.rs
9353 views
1
//! Provides types to specify atmosphere lighting, scattering terms, etc.
2
3
use alloc::{borrow::Cow, sync::Arc};
4
use bevy_asset::{Asset, Handle};
5
use bevy_camera::Hdr;
6
use bevy_ecs::component::Component;
7
use bevy_math::{ops, Curve, FloatPow, Vec3};
8
use bevy_reflect::TypePath;
9
use core::f32::{self, consts::PI};
10
use smallvec::SmallVec;
11
12
/// Enables atmospheric scattering for an HDR camera.
13
#[derive(Clone, Component)]
14
#[require(Hdr)]
15
pub struct Atmosphere {
16
/// Radius of the planet
17
///
18
/// units: m
19
pub bottom_radius: f32,
20
21
/// Radius at which we consider the atmosphere to 'end' for our
22
/// calculations (from center of planet)
23
///
24
/// units: m
25
pub top_radius: f32,
26
27
/// An approximation of the average albedo (or color, roughly) of the
28
/// planet's surface. This is used when calculating multiscattering.
29
///
30
/// units: N/A
31
pub ground_albedo: Vec3,
32
33
/// A handle to a [`ScatteringMedium`], which describes the substance
34
/// of the atmosphere and how it scatters light.
35
pub medium: Handle<ScatteringMedium>,
36
}
37
38
impl Atmosphere {
39
/// An atmosphere like that of earth; use this with a [`ScatteringMedium::earthlike`] handle.
40
pub fn earthlike(medium: Handle<ScatteringMedium>) -> Self {
41
const EARTH_BOTTOM_RADIUS: f32 = 6_360_000.0;
42
const EARTH_TOP_RADIUS: f32 = 6_460_000.0;
43
const EARTH_ALBEDO: Vec3 = Vec3::splat(0.3);
44
Self {
45
bottom_radius: EARTH_BOTTOM_RADIUS,
46
top_radius: EARTH_TOP_RADIUS,
47
ground_albedo: EARTH_ALBEDO,
48
medium,
49
}
50
}
51
}
52
53
/// An asset that defines how a material scatters light.
54
///
55
/// In order to calculate how light passes through a medium,
56
/// you need three pieces of information:
57
/// - how much light the medium *absorbs* per unit length
58
/// - how much light the medium *scatters* per unit length
59
/// - what *directions* the medium is likely to scatter light in.
60
///
61
/// The first two are fairly simple, and are sometimes referred to together
62
/// (accurately enough) as the medium's [optical density].
63
///
64
/// The last, defined by a [phase function], is the most important in creating
65
/// the look of a medium. Our brains are very good at noticing (if unconsciously)
66
/// that a dust storm scatters light differently than a rain cloud, for example.
67
/// See the docs on [`PhaseFunction`] for more info.
68
///
69
/// In reality, media are often composed of multiple elements that scatter light
70
/// independently, for Earth's atmosphere is composed of the gas itself, but also
71
/// suspended dust and particulate. These each scatter light differently, and are
72
/// distributed in different amounts at different altitudes. In a [`ScatteringMedium`],
73
/// these are each represented by a [`ScatteringTerm`]
74
///
75
/// ## Technical Details
76
///
77
/// A [`ScatteringMedium`] is represented on the GPU by a set of two LUTs, which
78
/// are re-created every time the asset is modified. See the docs on
79
/// `bevy_pbr::GpuScatteringMedium` for more info.
80
///
81
/// [optical density]: https://en.wikipedia.org/wiki/Optical_Density
82
/// [phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions
83
#[derive(TypePath, Asset, Clone)]
84
pub struct ScatteringMedium {
85
/// An optional label for the medium, used when creating the LUTs on the GPU.
86
pub label: Option<Cow<'static, str>>,
87
/// The resolution at which to sample the falloff distribution of each
88
/// scattering term. Custom or more detailed distributions may benefit
89
/// from a higher value, at the cost of more memory use.
90
pub falloff_resolution: u32,
91
/// The resolution at which to sample the phase function of each scattering
92
/// term. Custom or more detailed phase functions may benefit from a higher
93
/// value, at the cost of more memory use.
94
pub phase_resolution: u32,
95
/// The list of [`ScatteringTerm`]s that compose this [`ScatteringMedium`]
96
pub terms: SmallVec<[ScatteringTerm; 1]>,
97
}
98
99
impl Default for ScatteringMedium {
100
fn default() -> Self {
101
ScatteringMedium::earthlike(256, 256)
102
}
103
}
104
105
impl ScatteringMedium {
106
/// Returns a scattering medium with a default label and the
107
/// specified scattering terms.
108
pub fn new(
109
falloff_resolution: u32,
110
phase_resolution: u32,
111
terms: impl IntoIterator<Item = ScatteringTerm>,
112
) -> Self {
113
Self {
114
label: None,
115
falloff_resolution,
116
phase_resolution,
117
terms: terms.into_iter().collect(),
118
}
119
}
120
121
/// Consumes and returns this scattering medium with a new label.
122
pub fn with_label(self, label: impl Into<Cow<'static, str>>) -> Self {
123
Self {
124
label: Some(label.into()),
125
..self
126
}
127
}
128
129
/// Consumes and returns this scattering medium with each scattering terms'
130
/// densities multiplied by `multiplier`.
131
pub fn with_density_multiplier(mut self, multiplier: f32) -> Self {
132
self.terms.iter_mut().for_each(|term| {
133
term.absorption *= multiplier;
134
term.scattering *= multiplier;
135
});
136
137
self
138
}
139
140
/// Returns a scattering medium representing an earthlike atmosphere.
141
///
142
/// Uses physically-based scale heights from Earth's atmosphere, assuming
143
/// a 60 km atmosphere height:
144
/// - Rayleigh (molecular) scattering: 8 km scale height
145
/// - Mie (aerosol) scattering: 1.2 km scale height
146
pub fn earthlike(falloff_resolution: u32, phase_resolution: u32) -> Self {
147
Self::new(
148
falloff_resolution,
149
phase_resolution,
150
[
151
// Rayleigh scattering Term
152
ScatteringTerm {
153
absorption: Vec3::ZERO,
154
scattering: Vec3::new(5.802e-6, 13.558e-6, 33.100e-6),
155
falloff: Falloff::Exponential { scale: 8.0 / 60.0 },
156
phase: PhaseFunction::Rayleigh,
157
},
158
// Mie scattering Term
159
ScatteringTerm {
160
absorption: Vec3::splat(3.996e-6),
161
scattering: Vec3::splat(0.444e-6),
162
falloff: Falloff::Exponential { scale: 1.2 / 60.0 },
163
phase: PhaseFunction::Mie { asymmetry: 0.8 },
164
},
165
// Ozone scattering Term
166
ScatteringTerm {
167
absorption: Vec3::new(0.650e-6, 1.881e-6, 0.085e-6),
168
scattering: Vec3::ZERO,
169
falloff: Falloff::Tent {
170
center: 0.75,
171
width: 0.3,
172
},
173
phase: PhaseFunction::Isotropic,
174
},
175
],
176
)
177
.with_label("earthlike_atmosphere")
178
}
179
}
180
181
/// An individual element of a [`ScatteringMedium`].
182
///
183
/// A [`ScatteringMedium`] can be built out of a number of simpler [`ScatteringTerm`]s,
184
/// which correspond to an individual element of the medium. For example, Earth's
185
/// atmosphere would be (roughly) composed of two [`ScatteringTerm`]s: the atmospheric
186
/// gases themselves, which extend to the edge of space, and suspended dust particles,
187
/// which are denser but lie closer to the ground.
188
#[derive(Default, Clone)]
189
pub struct ScatteringTerm {
190
/// This term's optical absorption density, or how much light of each wavelength
191
/// it absorbs per meter.
192
///
193
/// units: m^-1
194
pub absorption: Vec3,
195
/// This term's optical scattering density, or how much light of each wavelength
196
/// it scatters per meter.
197
///
198
/// units: m^-1
199
pub scattering: Vec3,
200
/// This term's falloff distribution. See the docs on [`Falloff`] for more info.
201
pub falloff: Falloff,
202
/// This term's [phase function], which determines the character of how it
203
/// scatters light. See the docs on [`PhaseFunction`] for more info.
204
///
205
/// [phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions
206
pub phase: PhaseFunction,
207
}
208
209
/// Describes how the media in a [`ScatteringTerm`] is distributed.
210
///
211
/// This is closely related to the optical density values [`ScatteringTerm::absorption`] and
212
/// [`ScatteringTerm::scattering`]. Most media aren't the same density everywhere;
213
/// near the edge of space Earth's atmosphere is much less dense, and it absorbs
214
/// and scatters less light.
215
///
216
/// [`Falloff`] determines how the density of a medium changes as a function of
217
/// an abstract "falloff parameter" `p`. `p = 1` denotes where the medium is the
218
/// densest, i.e. at the surface of the Earth, `p = 0` denotes where the medium
219
/// fades away completely, i.e. at the edge of space, and values between scale
220
/// linearly with distance, so `p = 0.5` would be halfway between the surface
221
/// and the edge of space.
222
///
223
/// When processing a [`ScatteringMedium`], the `absorption` and `scattering` values
224
/// for each [`ScatteringTerm`] are multiplied by the value of the falloff function, `f(p)`.
225
#[derive(Default, Clone)]
226
pub enum Falloff {
227
/// A simple linear falloff function, which essentially
228
/// passes the falloff parameter through unchanged.
229
///
230
/// f(1) = 1
231
/// f(0) = 0
232
/// f(p) = p
233
#[default]
234
Linear,
235
/// An exponential falloff function parametrized by a proportional scale.
236
/// When paired with an absolute "falloff distance" like the distance from
237
/// Earth's surface to the edge of space, this is analogous to the "height
238
/// scale" value common in atmospheric scattering literature, though it will
239
/// diverge from this for large or negative `scale` values.
240
///
241
/// f(1) = 1
242
/// f(0) = 0
243
/// f(p) = (e^((1-p)/s) - e^(1/s))/(e - e^(1/s))
244
Exponential {
245
/// The "scale" of the exponential falloff. Values closer to zero will
246
/// produce steeper falloff, and values farther from zero will produce
247
/// gentler falloff, approaching linear falloff as scale goes to `+-∞`.
248
///
249
/// Negative values change the *concavity* of the falloff function:
250
/// rather than an initial narrow region of steep falloff followed by a
251
/// wide region of gentle falloff, there will be an initial wide region
252
/// of gentle falloff followed by a narrow region of steep falloff.
253
///
254
/// domain: (-∞, ∞)
255
///
256
/// NOTE, this function is not defined when `scale == 0`.
257
/// In that case, it will fall back to linear falloff.
258
scale: f32,
259
},
260
/// A tent-shaped falloff function, which produces a triangular
261
/// peak at the center and linearly falls off to either side.
262
///
263
/// f(`center`) = 1
264
/// f(`center` +- `width` / 2) = 0
265
Tent {
266
/// The center of the tent function peak
267
///
268
/// domain: [0, 1]
269
center: f32,
270
/// The total width of the tent function peak
271
///
272
/// domain: [0, 1]
273
width: f32,
274
},
275
/// A falloff function defined by a custom curve.
276
///
277
/// domain: [0, 1],
278
/// range: [0, 1],
279
Curve(Arc<dyn Curve<f32> + Send + Sync>),
280
}
281
282
impl Falloff {
283
/// Returns a falloff function corresponding to a custom curve.
284
pub fn from_curve(curve: impl Curve<f32> + Send + Sync + 'static) -> Self {
285
Self::Curve(Arc::new(curve))
286
}
287
288
/// Evaluates the falloff function at the given coordinate.
289
pub fn sample(&self, p: f32) -> f32 {
290
match self {
291
Falloff::Linear => p,
292
Falloff::Exponential { scale } => {
293
// fill discontinuity at scale == 0,
294
// arbitrarily choose linear falloff
295
if *scale == 0.0 {
296
p
297
} else {
298
let s = -1.0 / scale;
299
let exp_p_s = ops::exp((1.0 - p) * s);
300
let exp_s = ops::exp(s);
301
(exp_p_s - exp_s) / (1.0 - exp_s)
302
}
303
}
304
Falloff::Tent { center, width } => (1.0 - (p - center).abs() / (0.5 * width)).max(0.0),
305
Falloff::Curve(curve) => curve.sample(p).unwrap_or(0.0),
306
}
307
}
308
}
309
310
/// Describes how a [`ScatteringTerm`] scatters light in different directions.
311
///
312
/// A [phase function] is a function `f: [-1, 1] -> [0, ∞)`, symmetric about `x=0`
313
/// whose input is the cosine of the angle between an incoming light direction and
314
/// and outgoing light direction, and whose output is the proportion of the incoming
315
/// light that is actually scattered in that direction.
316
///
317
/// The phase function has an important effect on the "look" of a medium in a scene.
318
/// Media consisting of particles of a different size or shape scatter light differently,
319
/// and our brains are very good at telling the difference. A dust cloud, which might
320
/// correspond roughly to `PhaseFunction::Mie { asymmetry: 0.8 }`, looks quite different
321
/// from the rest of the sky (atmospheric gases), which correspond to `PhaseFunction::Rayleigh`
322
///
323
/// [phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions
324
#[derive(Clone)]
325
pub enum PhaseFunction {
326
/// A phase function that scatters light evenly in all directions.
327
Isotropic,
328
329
/// A phase function representing [Rayleigh scattering].
330
///
331
/// Rayleigh scattering occurs naturally for particles much smaller than
332
/// the wavelengths of visible light, such as gas molecules in the atmosphere.
333
/// It's generally wavelength-dependent, where shorter wavelengths are scattered
334
/// more strongly, so [scattering](ScatteringTerm::scattering) should have
335
/// higher values for blue than green and green than red. Particles that
336
/// participate in Rayleigh scattering don't absorb any light, either.
337
///
338
/// [Rayleigh scattering]: https://en.wikipedia.org/wiki/Rayleigh_scattering
339
Rayleigh,
340
341
/// The [Henyey-Greenstein phase function], which approximates [Mie scattering].
342
///
343
/// Mie scattering occurs naturally for spherical particles of dust
344
/// and aerosols roughly the same size as the wavelengths of visible light,
345
/// so it's useful for representing dust or sea spray. It's generally
346
/// wavelength-independent, so [absorption](ScatteringTerm::absorption)
347
/// and [scattering](ScatteringTerm::scattering) should be set to a greyscale value.
348
///
349
/// [Mie scattering]: https://en.wikipedia.org/wiki/Mie_scattering
350
/// [Henyey-Greenstein phase function]: https://www.oceanopticsbook.info/view/scattering/level-2/the-henyey-greenstein-phase-function
351
Mie {
352
/// Whether the Mie scattering function is biased towards scattering
353
/// light forwards (asymmetry > 0) or backwards (asymmetry < 0).
354
///
355
/// domain: [-1, 1]
356
asymmetry: f32,
357
},
358
359
/// A phase function defined by a custom curve, where the input
360
/// is the cosine of the angle between the incoming light ray
361
/// and the scattered light ray, and the output is the fraction
362
/// of the incoming light scattered in that direction.
363
///
364
/// Note: it's important for photorealism that the phase function
365
/// be *energy conserving*, meaning that in total no more light can
366
/// be scattered than actually entered the medium. For this to be
367
/// the case, the integral of the phase function over its domain must
368
/// be equal to 1/2π.
369
///
370
/// 1
371
/// ∫ p(x) dx = 1/2π
372
/// -1
373
///
374
/// domain: [-1, 1]
375
/// range: [0, 1]
376
Curve(Arc<dyn Curve<f32> + Send + Sync>),
377
}
378
379
impl PhaseFunction {
380
/// A phase function defined by a custom curve.
381
pub fn from_curve(curve: impl Curve<f32> + Send + Sync + 'static) -> Self {
382
Self::Curve(Arc::new(curve))
383
}
384
385
/// Samples the phase function at the given value in [-1, 1], output is in [0, 1].
386
pub fn sample(&self, neg_l_dot_v: f32) -> f32 {
387
const FRAC_4_PI: f32 = 0.25 / PI;
388
const FRAC_3_16_PI: f32 = 0.1875 / PI;
389
match self {
390
PhaseFunction::Isotropic => FRAC_4_PI,
391
PhaseFunction::Rayleigh => FRAC_3_16_PI * (1.0 + neg_l_dot_v * neg_l_dot_v),
392
PhaseFunction::Mie { asymmetry } => {
393
let denom = 1.0 + asymmetry.squared() - 2.0 * asymmetry * neg_l_dot_v;
394
FRAC_4_PI * (1.0 - asymmetry.squared()) / (denom * denom.sqrt())
395
}
396
PhaseFunction::Curve(curve) => curve.sample(neg_l_dot_v).unwrap_or(0.0),
397
}
398
}
399
}
400
401
impl Default for PhaseFunction {
402
fn default() -> Self {
403
Self::Mie { asymmetry: 0.8 }
404
}
405
}
406
407