Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_pbr/src/medium.rs
7219 views
1
use alloc::{borrow::Cow, sync::Arc};
2
use core::f32::{self, consts::PI};
3
4
use bevy_app::{App, Plugin};
5
use bevy_asset::{Asset, AssetApp, AssetId};
6
use bevy_ecs::{
7
resource::Resource,
8
system::{Commands, Res, SystemParamItem},
9
};
10
use bevy_math::{ops, Curve, FloatPow, Vec3, Vec4};
11
use bevy_reflect::TypePath;
12
use bevy_render::{
13
render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin},
14
render_resource::{
15
Extent3d, FilterMode, Sampler, SamplerDescriptor, Texture, TextureDataOrder,
16
TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, TextureView,
17
TextureViewDescriptor,
18
},
19
renderer::{RenderDevice, RenderQueue},
20
RenderApp, RenderStartup,
21
};
22
use smallvec::SmallVec;
23
24
#[doc(hidden)]
25
pub struct ScatteringMediumPlugin;
26
27
impl Plugin for ScatteringMediumPlugin {
28
fn build(&self, app: &mut App) {
29
app.init_asset::<ScatteringMedium>()
30
.add_plugins(RenderAssetPlugin::<GpuScatteringMedium>::default());
31
32
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
33
render_app.add_systems(RenderStartup, init_scattering_medium_sampler);
34
}
35
}
36
}
37
38
/// An asset that defines how a material scatters light.
39
///
40
/// In order to calculate how light passes through a medium,
41
/// you need three pieces of information:
42
/// - how much light the medium *absorbs* per unit length
43
/// - how much light the medium *scatters* per unit length
44
/// - what *directions* the medium is likely to scatter light in.
45
///
46
/// The first two are fairly simple, and are sometimes referred to together
47
/// (accurately enough) as the medium's [optical density].
48
///
49
/// The last, defined by a [phase function], is the most important in creating
50
/// the look of a medium. Our brains are very good at noticing (if unconsciously)
51
/// that a dust storm scatters light differently than a rain cloud, for example.
52
/// See the docs on [`PhaseFunction`] for more info.
53
///
54
/// In reality, media are often composed of multiple elements that scatter light
55
/// independently, for Earth's atmosphere is composed of the gas itself, but also
56
/// suspended dust and particulate. These each scatter light differently, and are
57
/// distributed in different amounts at different altitudes. In a [`ScatteringMedium`],
58
/// these are each represented by a [`ScatteringTerm`]
59
///
60
/// ## Technical Details
61
///
62
/// A [`ScatteringMedium`] is represented on the GPU by a set of two LUTs, which
63
/// are re-created every time the asset is modified. See the docs on
64
/// [`GpuScatteringMedium`] for more info.
65
///
66
/// [optical density]: https://en.wikipedia.org/wiki/Optical_Density
67
/// [phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions
68
#[derive(TypePath, Asset, Clone)]
69
pub struct ScatteringMedium {
70
/// An optional label for the medium, used when creating the LUTs on the GPU.
71
pub label: Option<Cow<'static, str>>,
72
/// The resolution at which to sample the falloff distribution of each
73
/// scattering term. Custom or more detailed distributions may benefit
74
/// from a higher value, at the cost of more memory use.
75
pub falloff_resolution: u32,
76
/// The resolution at which to sample the phase function of each scattering
77
/// term. Custom or more detailed phase functions may benefit from a higher
78
/// value, at the cost of more memory use.
79
pub phase_resolution: u32,
80
/// The list of [`ScatteringTerm`]s that compose this [`ScatteringMedium`]
81
pub terms: SmallVec<[ScatteringTerm; 1]>,
82
}
83
84
impl ScatteringMedium {
85
// Returns a scattering medium with a default label and the
86
// specified scattering terms.
87
pub fn new(
88
falloff_resolution: u32,
89
phase_resolution: u32,
90
terms: impl IntoIterator<Item = ScatteringTerm>,
91
) -> Self {
92
Self {
93
label: None,
94
falloff_resolution,
95
phase_resolution,
96
terms: terms.into_iter().collect(),
97
}
98
}
99
100
// Consumes and returns this scattering medium with a new label.
101
pub fn with_label(self, label: impl Into<Cow<'static, str>>) -> Self {
102
Self {
103
label: Some(label.into()),
104
..self
105
}
106
}
107
108
// Consumes and returns this scattering medium with each scattering terms'
109
// densities multiplied by `multiplier`.
110
pub fn with_density_multiplier(mut self, multiplier: f32) -> Self {
111
self.terms.iter_mut().for_each(|term| {
112
term.absorption *= multiplier;
113
term.scattering *= multiplier;
114
});
115
116
self
117
}
118
119
/// Returns a scattering medium representing an earthlike atmosphere.
120
pub fn earthlike(falloff_resolution: u32, phase_resolution: u32) -> Self {
121
Self::new(
122
falloff_resolution,
123
phase_resolution,
124
[
125
ScatteringTerm {
126
absorption: Vec3::ZERO,
127
scattering: Vec3::new(5.802e-6, 13.558e-6, 33.100e-6),
128
falloff: Falloff::Exponential { strength: 12.5 },
129
phase: PhaseFunction::Rayleigh,
130
},
131
ScatteringTerm {
132
absorption: Vec3::splat(3.996e-6),
133
scattering: Vec3::splat(0.444e-6),
134
falloff: Falloff::Exponential { strength: 83.5 },
135
phase: PhaseFunction::Mie { asymmetry: 0.8 },
136
},
137
ScatteringTerm {
138
absorption: Vec3::new(0.650e-6, 1.881e-6, 0.085e-6),
139
scattering: Vec3::ZERO,
140
falloff: Falloff::Tent {
141
center: 0.75,
142
width: 0.3,
143
},
144
phase: PhaseFunction::Isotropic,
145
},
146
],
147
)
148
.with_label("earthlike_atmosphere")
149
}
150
}
151
152
/// An individual element of a [`ScatteringMedium`].
153
///
154
/// A [`ScatteringMedium`] can be built out of a number of simpler [`ScatteringTerm`]s,
155
/// which correspond to an individual element of the medium. For example, Earth's
156
/// atmosphere would be (roughly) composed of two [`ScatteringTerm`]s: the atmospheric
157
/// gases themselves, which extend to the edge of space, and suspended dust particles,
158
/// which are denser but lie closer to the ground.
159
#[derive(Default, Clone)]
160
pub struct ScatteringTerm {
161
/// This term's optical obsorption density, or how much light of each wavelength
162
/// it absorbs per meter.
163
///
164
/// units: m^-1
165
pub absorption: Vec3,
166
/// This term's optical scattering density, or how much light of each wavelength
167
/// it scatters per meter.
168
///
169
/// units: m^-1
170
pub scattering: Vec3,
171
/// This term's falloff distribution. See the docs on [`Falloff`] for more info.
172
pub falloff: Falloff,
173
/// This term's [phase function], which determines the character of how it
174
/// scatters light. See the docs on [`PhaseFunction`] for more info.
175
///
176
/// [phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions
177
pub phase: PhaseFunction,
178
}
179
180
/// Describes how the media in a [`ScatteringTerm`] is distributed.
181
///
182
/// This is closely related to the optical density values [`ScatteringTerm::absorption`] and
183
/// [`ScatteringTerm::scattering`]. Most media aren't the same density everywhere;
184
/// near the edge of space Earth's atmosphere is much less dense, and it absorbs
185
/// and scatters less light.
186
///
187
/// [`Falloff`] determines how the density of a medium changes as a function of
188
/// an abstract "falloff parameter" `p`. `p = 1` denotes where the medium is the
189
/// densest, i.e. at the surface of the Earth, `p = 0` denotes where the medium
190
/// fades away completely, i.e. at the edge of space, and values between scale
191
/// linearly with distance, so `p = 0.5` would be halfway between the surface
192
/// and the edge of space.
193
///
194
/// When processing a [`ScatteringMedium`], the `absorption` and `scattering` values
195
/// for each [`ScatteringTerm`] are multiplied by the value of the falloff function, `f(p)`.
196
#[derive(Default, Clone)]
197
pub enum Falloff {
198
/// A simple linear falloff function, which essentially
199
/// passes the falloff parameter through unchanged.
200
///
201
/// f(1) = 1
202
/// f(0) = 0
203
/// f(p) = p
204
#[default]
205
Linear,
206
/// An exponential falloff function with adjustable strength.
207
///
208
/// f(1) = 1
209
/// f(0) = 0
210
/// f(p) = (e^sp - 1)/(e^s - 1)
211
Exponential {
212
/// The "strength" of the exponential falloff. The higher
213
/// this value is, the quicker the medium's density will
214
/// decrease with distance.
215
///
216
/// domain: (-∞, ∞)
217
strength: f32,
218
},
219
/// A tent-shaped falloff function, which produces a triangular
220
/// peak at the center and linearly falls off to either side.
221
///
222
/// f(`center`) = 1
223
/// f(`center` +- `width` / 2) = 0
224
Tent {
225
/// The center of the tent function peak
226
///
227
/// domain: [0, 1]
228
center: f32,
229
/// The total width of the tent function peak
230
///
231
/// domain: [0, 1]
232
width: f32,
233
},
234
/// A falloff function defined by a custom curve.
235
///
236
/// domain: [0, 1],
237
/// range: [0, 1],
238
Curve(Arc<dyn Curve<f32> + Send + Sync>),
239
}
240
241
impl Falloff {
242
/// Returns a falloff function corresponding to a custom curve.
243
pub fn from_curve(curve: impl Curve<f32> + Send + Sync + 'static) -> Self {
244
Self::Curve(Arc::new(curve))
245
}
246
247
fn sample(&self, falloff: f32) -> f32 {
248
match self {
249
Falloff::Linear => falloff,
250
Falloff::Exponential { strength } => {
251
// fill discontinuity at strength == 0
252
if *strength == 0.0 {
253
falloff
254
} else {
255
let scale_exp_m1 = ops::exp_m1(*strength);
256
let domain_offset = ops::ln(scale_exp_m1.abs());
257
let range_offset = scale_exp_m1.recip();
258
let eval_pos = falloff * strength - domain_offset;
259
scale_exp_m1.signum() * ops::exp(eval_pos) - range_offset
260
}
261
}
262
Falloff::Tent { center, width } => {
263
(1.0 - (falloff - center).abs() / (0.5 * width)).max(0.0)
264
}
265
Falloff::Curve(curve) => curve.sample(falloff).unwrap_or(0.0),
266
}
267
}
268
}
269
270
/// Describes how a [`ScatteringTerm`] scatters light in different directions.
271
///
272
/// A [phase function] is a function `f: [-1, 1] -> [0, ∞)`, symmetric about `x=0`
273
/// whose input is the cosine of the angle between an incoming light direction and
274
/// and outgoing light direction, and whose output is the proportion of the incoming
275
/// light that is actually scattered in that direction.
276
///
277
/// The phase function has an important effect on the "look" of a medium in a scene.
278
/// Media consisting of particles of a different size or shape scatter light differently,
279
/// and our brains are very good at telling the difference. A dust cloud, which might
280
/// correspond roughly to `PhaseFunction::Mie { asymmetry: 0.8 }`, looks quite different
281
/// from the rest of the sky (atmospheric gases), which correspond to `PhaseFunction::Rayleigh`
282
///
283
/// [phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions
284
#[derive(Clone)]
285
pub enum PhaseFunction {
286
/// A phase function that scatters light evenly in all directions.
287
Isotropic,
288
289
/// A phase function representing [Rayleigh scattering].
290
///
291
/// Rayleigh scattering occurs naturally for particles much smaller than
292
/// the wavelengths of visible light, such as gas molecules in the atmosphere.
293
/// It's generally wavelength-dependent, where shorter wavelengths are scattered
294
/// more strongly, so [scattering](ScatteringTerm::scattering) should have
295
/// higher values for blue than green and green than red. Particles that
296
/// participate in Rayleigh scattering don't absorb any light, either.
297
///
298
/// [Rayleigh scattering]: https://en.wikipedia.org/wiki/Rayleigh_scattering
299
Rayleigh,
300
301
/// The [Henyey-Greenstein phase function], which approximates [Mie scattering].
302
///
303
/// Mie scattering occurs naturally for spherical particles of dust
304
/// and aerosols roughly the same size as the wavelengths of visible light,
305
/// so it's useful for representing dust or sea spray. It's generally
306
/// wavelength-independent, so [absorption](ScatteringTerm::absorption)
307
/// and [scattering](ScatteringTerm::scattering) should be set to a greyscale value.
308
///
309
/// [Mie scattering]: https://en.wikipedia.org/wiki/Mie_scattering
310
/// [Henyey-Greenstein phase function]: https://www.oceanopticsbook.info/view/scattering/level-2/the-henyey-greenstein-phase-function
311
Mie {
312
/// Whether the Mie scattering function is biased towards scattering
313
/// light forwards (asymmetry > 0) or backwards (asymmetry < 0).
314
///
315
/// domain: [-1, 1]
316
asymmetry: f32,
317
},
318
319
/// A phase function defined by a custom curve, where the input
320
/// is the cosine of the angle between the incoming light ray
321
/// and the scattered light ray, and the output is the fraction
322
/// of the incoming light scattered in that direction.
323
///
324
/// Note: it's important for photorealism that the phase function
325
/// be *energy conserving*, meaning that in total no more light can
326
/// be scattered than actually entered the medium. For this to be
327
/// the case, the integral of the phase function over its domain must
328
/// be equal to 1/2π.
329
///
330
/// 1
331
/// ∫ p(x) dx = 1/2π
332
/// -1
333
///
334
/// domain: [-1, 1]
335
/// range: [0, 1]
336
Curve(Arc<dyn Curve<f32> + Send + Sync>),
337
}
338
339
impl PhaseFunction {
340
/// A phase function defined by a custom curve.
341
pub fn from_curve(curve: impl Curve<f32> + Send + Sync + 'static) -> Self {
342
Self::Curve(Arc::new(curve))
343
}
344
345
fn sample(&self, neg_l_dot_v: f32) -> f32 {
346
const FRAC_4_PI: f32 = 0.25 / PI;
347
const FRAC_3_16_PI: f32 = 0.1875 / PI;
348
match self {
349
PhaseFunction::Isotropic => FRAC_4_PI,
350
PhaseFunction::Rayleigh => FRAC_3_16_PI * (1.0 + neg_l_dot_v * neg_l_dot_v),
351
PhaseFunction::Mie { asymmetry } => {
352
let denom = 1.0 + asymmetry.squared() - 2.0 * asymmetry * neg_l_dot_v;
353
FRAC_4_PI * (1.0 - asymmetry.squared()) / (denom * denom.sqrt())
354
}
355
PhaseFunction::Curve(curve) => curve.sample(neg_l_dot_v).unwrap_or(0.0),
356
}
357
}
358
}
359
360
impl Default for PhaseFunction {
361
fn default() -> Self {
362
Self::Mie { asymmetry: 0.8 }
363
}
364
}
365
366
/// The GPU representation of a [`ScatteringMedium`].
367
pub struct GpuScatteringMedium {
368
/// The terms of the scattering medium.
369
pub terms: SmallVec<[ScatteringTerm; 1]>,
370
/// The resolution at which to sample the falloff distribution of each
371
/// scattering term.
372
pub falloff_resolution: u32,
373
/// The resolution at which to sample the phase function of each
374
/// scattering term.
375
pub phase_resolution: u32,
376
/// The `density_lut`, a 2D `falloff_resolution x 2` LUT which contains the
377
/// medium's optical density with respect to the atmosphere's "falloff parameter",
378
/// a linear value which is 1.0 at the planet's surface and 0.0 at the edge of
379
/// space. The first and second rows correspond to absorption density and
380
/// scattering density respectively.
381
pub density_lut: Texture,
382
/// The default [`TextureView`] of the `density_lut`
383
pub density_lut_view: TextureView,
384
/// The `scattering_lut`, a 2D `falloff_resolution x phase_resolution` LUT which
385
/// contains the medium's scattering density multiplied by the phase function, with
386
/// the U axis corresponding to the falloff parameter and the V axis corresponding
387
/// to `neg_LdotV * 0.5 + 0.5`, where `neg_LdotV` is the dot product of the light
388
/// direction and the incoming view vector.
389
pub scattering_lut: Texture,
390
/// The default [`TextureView`] of the `scattering_lut`
391
pub scattering_lut_view: TextureView,
392
}
393
394
impl RenderAsset for GpuScatteringMedium {
395
type SourceAsset = ScatteringMedium;
396
397
type Param = (Res<'static, RenderDevice>, Res<'static, RenderQueue>);
398
399
fn prepare_asset(
400
source_asset: Self::SourceAsset,
401
_asset_id: AssetId<Self::SourceAsset>,
402
(render_device, render_queue): &mut SystemParamItem<Self::Param>,
403
_previous_asset: Option<&Self>,
404
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
405
let mut density: Vec<Vec4> =
406
Vec::with_capacity(2 * source_asset.falloff_resolution as usize);
407
408
density.extend((0..source_asset.falloff_resolution).map(|i| {
409
let falloff = (i as f32 + 0.5) / source_asset.falloff_resolution as f32;
410
411
source_asset
412
.terms
413
.iter()
414
.map(|term| term.absorption.extend(0.0) * term.falloff.sample(falloff))
415
.sum::<Vec4>()
416
}));
417
418
density.extend((0..source_asset.falloff_resolution).map(|i| {
419
let falloff = (i as f32 + 0.5) / source_asset.falloff_resolution as f32;
420
421
source_asset
422
.terms
423
.iter()
424
.map(|term| term.scattering.extend(0.0) * term.falloff.sample(falloff))
425
.sum::<Vec4>()
426
}));
427
428
let mut scattering: Vec<Vec4> = Vec::with_capacity(
429
source_asset.falloff_resolution as usize * source_asset.phase_resolution as usize,
430
);
431
432
scattering.extend(
433
(0..source_asset.falloff_resolution * source_asset.phase_resolution).map(|raw_i| {
434
let i = raw_i % source_asset.phase_resolution;
435
let j = raw_i / source_asset.phase_resolution;
436
let falloff = (i as f32 + 0.5) / source_asset.falloff_resolution as f32;
437
let phase = (j as f32 + 0.5) / source_asset.phase_resolution as f32;
438
let neg_l_dot_v = phase * 2.0 - 1.0;
439
440
source_asset
441
.terms
442
.iter()
443
.map(|term| {
444
term.scattering.extend(0.0)
445
* term.falloff.sample(falloff)
446
* term.phase.sample(neg_l_dot_v)
447
})
448
.sum::<Vec4>()
449
}),
450
);
451
452
let density_lut = render_device.create_texture_with_data(
453
render_queue,
454
&TextureDescriptor {
455
label: source_asset
456
.label
457
.as_deref()
458
.map(|label| format!("{}_density_lut", label))
459
.as_deref()
460
.or(Some("scattering_medium_density_lut")),
461
size: Extent3d {
462
width: source_asset.falloff_resolution,
463
height: 2,
464
depth_or_array_layers: 1,
465
},
466
mip_level_count: 1,
467
sample_count: 1,
468
dimension: TextureDimension::D2,
469
format: TextureFormat::Rgba32Float,
470
usage: TextureUsages::TEXTURE_BINDING,
471
view_formats: &[],
472
},
473
TextureDataOrder::LayerMajor,
474
bytemuck::cast_slice(density.as_slice()),
475
);
476
477
let density_lut_view = density_lut.create_view(&TextureViewDescriptor {
478
label: source_asset
479
.label
480
.as_deref()
481
.map(|label| format!("{}_density_lut_view", label))
482
.as_deref()
483
.or(Some("scattering_medium_density_lut_view")),
484
..Default::default()
485
});
486
487
let scattering_lut = render_device.create_texture_with_data(
488
render_queue,
489
&TextureDescriptor {
490
label: source_asset
491
.label
492
.as_deref()
493
.map(|label| format!("{}_scattering_lut", label))
494
.as_deref()
495
.or(Some("scattering_medium_scattering_lut")),
496
size: Extent3d {
497
width: source_asset.falloff_resolution,
498
height: source_asset.phase_resolution,
499
depth_or_array_layers: 1,
500
},
501
mip_level_count: 1,
502
sample_count: 1,
503
dimension: TextureDimension::D2,
504
format: TextureFormat::Rgba32Float,
505
usage: TextureUsages::TEXTURE_BINDING,
506
view_formats: &[],
507
},
508
TextureDataOrder::LayerMajor,
509
bytemuck::cast_slice(scattering.as_slice()),
510
);
511
512
let scattering_lut_view = scattering_lut.create_view(&TextureViewDescriptor {
513
label: source_asset
514
.label
515
.as_deref()
516
.map(|label| format!("{}_scattering_lut", label))
517
.as_deref()
518
.or(Some("scattering_medium_scattering_lut_view")),
519
..Default::default()
520
});
521
522
Ok(Self {
523
terms: source_asset.terms,
524
falloff_resolution: source_asset.falloff_resolution,
525
phase_resolution: source_asset.phase_resolution,
526
density_lut,
527
density_lut_view,
528
scattering_lut,
529
scattering_lut_view,
530
})
531
}
532
}
533
534
/// The default sampler for all scattering media LUTs.
535
///
536
/// Just a bilinear clamp-to-edge sampler, nothing fancy.
537
#[derive(Resource)]
538
pub struct ScatteringMediumSampler(Sampler);
539
540
impl ScatteringMediumSampler {
541
pub fn sampler(&self) -> &Sampler {
542
&self.0
543
}
544
}
545
546
fn init_scattering_medium_sampler(mut commands: Commands, render_device: Res<RenderDevice>) {
547
let sampler = render_device.create_sampler(&SamplerDescriptor {
548
label: Some("scattering_medium_sampler"),
549
mag_filter: FilterMode::Linear,
550
min_filter: FilterMode::Linear,
551
..Default::default()
552
});
553
554
commands.insert_resource(ScatteringMediumSampler(sampler));
555
}
556
557