Skip to content

Commit 81a25bb

Browse files
ecoskeyatlv24JMS55Emerson Coskeymate-h
authored
Procedural atmospheric scattering (#16314)
Implement procedural atmospheric scattering from [Sebastien Hillaire's 2020 paper](https://sebh.github.io/publications/egsr2020.pdf). This approach should scale well even down to mobile hardware, and is physically accurate. ## Co-author: @mate-h He helped massively with getting this over the finish line, ensuring everything was physically correct, correcting several places where I had misunderstood or misapplied the paper, and improving the performance in several places as well. Thanks! ## Credits @aevyrie: helped find numerous bugs and improve the example to best show off this feature :) Built off of @mtsr's original branch, which handled the transmittance lut (arguably the most important part) ## Showcase: ![sunset](https://github.com/user-attachments/assets/2eee1f38-f66d-4772-bb72-163e13c719d8) ![twilight](https://github.com/user-attachments/assets/f7d358b6-898d-4df7-becc-188cd753102d) ## For followup - Integrate with pcwalton's volumetrics code - refactor/reorganize for better integration with other effects - have atmosphere transmittance affect directional lights - add support for generating skybox/environment map --------- Co-authored-by: Emerson Coskey <[email protected]> Co-authored-by: atlv <[email protected]> Co-authored-by: JMS55 <[email protected]> Co-authored-by: Emerson Coskey <[email protected]> Co-authored-by: Máté Homolya <[email protected]>
1 parent d9ba1af commit 81a25bb

22 files changed

+2522
-12
lines changed

Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -862,6 +862,17 @@ description = "A scene showcasing the atmospheric fog effect"
862862
category = "3D Rendering"
863863
wasm = true
864864

865+
[[example]]
866+
name = "atmosphere"
867+
path = "examples/3d/atmosphere.rs"
868+
doc-scrape-examples = true
869+
870+
[package.metadata.example.atmosphere]
871+
name = "Atmosphere"
872+
description = "A scene showcasing pbr atmospheric scattering"
873+
category = "3D Rendering"
874+
wasm = true
875+
865876
[[example]]
866877
name = "fog"
867878
path = "examples/3d/fog.rs"

assets/models/terrain/terrain.glb

2.75 MB
Binary file not shown.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#import bevy_pbr::{
2+
mesh_view_types::{Lights, DirectionalLight},
3+
atmosphere::{
4+
types::{Atmosphere, AtmosphereSettings},
5+
bindings::{atmosphere, settings, view, lights, aerial_view_lut_out},
6+
functions::{
7+
sample_transmittance_lut, sample_atmosphere, rayleigh, henyey_greenstein,
8+
sample_multiscattering_lut, AtmosphereSample, sample_local_inscattering,
9+
get_local_r, get_local_up, view_radius, uv_to_ndc, max_atmosphere_distance,
10+
uv_to_ray_direction
11+
},
12+
}
13+
}
14+
15+
16+
@group(0) @binding(13) var aerial_view_lut_out: texture_storage_3d<rgba16float, write>;
17+
18+
@compute
19+
@workgroup_size(16, 16, 1)
20+
fn main(@builtin(global_invocation_id) idx: vec3<u32>) {
21+
if any(idx.xy > settings.aerial_view_lut_size.xy) { return; }
22+
23+
let uv = (vec2<f32>(idx.xy) + 0.5) / vec2<f32>(settings.aerial_view_lut_size.xy);
24+
let ray_dir = uv_to_ray_direction(uv);
25+
let r = view_radius();
26+
let mu = ray_dir.y;
27+
let t_max = settings.aerial_view_lut_max_distance;
28+
29+
var prev_t = 0.0;
30+
var total_inscattering = vec3(0.0);
31+
var throughput = vec3(1.0);
32+
33+
for (var slice_i: u32 = 0; slice_i < settings.aerial_view_lut_size.z; slice_i++) {
34+
for (var step_i: u32 = 0; step_i < settings.aerial_view_lut_samples; step_i++) {
35+
let t_i = t_max * (f32(slice_i) + ((f32(step_i) + 0.5) / f32(settings.aerial_view_lut_samples))) / f32(settings.aerial_view_lut_size.z);
36+
let dt = (t_i - prev_t);
37+
prev_t = t_i;
38+
39+
let local_r = get_local_r(r, mu, t_i);
40+
let local_up = get_local_up(r, t_i, ray_dir.xyz);
41+
42+
let local_atmosphere = sample_atmosphere(local_r);
43+
let sample_optical_depth = local_atmosphere.extinction * dt;
44+
let sample_transmittance = exp(-sample_optical_depth);
45+
46+
// evaluate one segment of the integral
47+
var inscattering = sample_local_inscattering(local_atmosphere, ray_dir.xyz, local_r, local_up);
48+
49+
// Analytical integration of the single scattering term in the radiance transfer equation
50+
let s_int = (inscattering - inscattering * sample_transmittance) / local_atmosphere.extinction;
51+
total_inscattering += throughput * s_int;
52+
53+
throughput *= sample_transmittance;
54+
if all(throughput < vec3(0.001)) {
55+
break;
56+
}
57+
}
58+
//We only have one channel to store transmittance, so we store the mean
59+
let mean_transmittance = (throughput.r + throughput.g + throughput.b) / 3.0;
60+
textureStore(aerial_view_lut_out, vec3(vec2<u32>(idx.xy), slice_i), vec4(total_inscattering, mean_transmittance));
61+
}
62+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#define_import_path bevy_pbr::atmosphere::bindings
2+
3+
#import bevy_render::view::View;
4+
5+
#import bevy_pbr::{
6+
mesh_view_types::Lights,
7+
atmosphere::types::{Atmosphere, AtmosphereSettings, AtmosphereTransforms}
8+
}
9+
10+
@group(0) @binding(0) var<uniform> atmosphere: Atmosphere;
11+
@group(0) @binding(1) var<uniform> settings: AtmosphereSettings;
12+
@group(0) @binding(2) var<uniform> atmosphere_transforms: AtmosphereTransforms;
13+
@group(0) @binding(3) var<uniform> view: View;
14+
@group(0) @binding(4) var<uniform> lights: Lights;
15+
@group(0) @binding(5) var transmittance_lut: texture_2d<f32>;
16+
@group(0) @binding(6) var transmittance_lut_sampler: sampler;
17+
@group(0) @binding(7) var multiscattering_lut: texture_2d<f32>;
18+
@group(0) @binding(8) var multiscattering_lut_sampler: sampler;
19+
@group(0) @binding(9) var sky_view_lut: texture_2d<f32>;
20+
@group(0) @binding(10) var sky_view_lut_sampler: sampler;
21+
@group(0) @binding(11) var aerial_view_lut: texture_3d<f32>;
22+
@group(0) @binding(12) var aerial_view_lut_sampler: sampler;
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright (c) 2017 Eric Bruneton
2+
// All rights reserved.
3+
//
4+
// Redistribution and use in source and binary forms, with or without
5+
// modification, are permitted provided that the following conditions
6+
// are met:
7+
// 1. Redistributions of source code must retain the above copyright
8+
// notice, this list of conditions and the following disclaimer.
9+
// 2. Redistributions in binary form must reproduce the above copyright
10+
// notice, this list of conditions and the following disclaimer in the
11+
// documentation and/or other materials provided with the distribution.
12+
// 3. Neither the name of the copyright holders nor the names of its
13+
// contributors may be used to endorse or promote products derived from
14+
// this software without specific prior written permission.
15+
//
16+
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19+
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
20+
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21+
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22+
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23+
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24+
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25+
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
26+
// THE POSSIBILITY OF SUCH DAMAGE.
27+
//
28+
// Precomputed Atmospheric Scattering
29+
// Copyright (c) 2008 INRIA
30+
// All rights reserved.
31+
//
32+
// Redistribution and use in source and binary forms, with or without
33+
// modification, are permitted provided that the following conditions
34+
// are met:
35+
// 1. Redistributions of source code must retain the above copyright
36+
// notice, this list of conditions and the following disclaimer.
37+
// 2. Redistributions in binary form must reproduce the above copyright
38+
// notice, this list of conditions and the following disclaimer in the
39+
// documentation and/or other materials provided with the distribution.
40+
// 3. Neither the name of the copyright holders nor the names of its
41+
// contributors may be used to endorse or promote products derived from
42+
// this software without specific prior written permission.
43+
//
44+
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
45+
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
46+
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
47+
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
48+
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
49+
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
50+
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
51+
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
52+
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
53+
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
54+
// THE POSSIBILITY OF SUCH DAMAGE.
55+
56+
#define_import_path bevy_pbr::atmosphere::bruneton_functions
57+
58+
#import bevy_pbr::atmosphere::{
59+
types::Atmosphere,
60+
bindings::atmosphere,
61+
}
62+
63+
// Mapping from view height (r) and zenith cos angle (mu) to UV coordinates in the transmittance LUT
64+
// Assuming r between ground and top atmosphere boundary, and mu= cos(zenith_angle)
65+
// Chosen to increase precision near the ground and to work around a discontinuity at the horizon
66+
// See Bruneton and Neyret 2008, "Precomputed Atmospheric Scattering" section 4
67+
fn transmittance_lut_r_mu_to_uv(r: f32, mu: f32) -> vec2<f32> {
68+
// Distance along a horizontal ray from the ground to the top atmosphere boundary
69+
let H = sqrt(atmosphere.top_radius * atmosphere.top_radius - atmosphere.bottom_radius * atmosphere.bottom_radius);
70+
71+
// Distance from a point at height r to the horizon
72+
// ignore the case where r <= atmosphere.bottom_radius
73+
let rho = sqrt(max(r * r - atmosphere.bottom_radius * atmosphere.bottom_radius, 0.0));
74+
75+
// Distance from a point at height r to the top atmosphere boundary at zenith angle mu
76+
let d = distance_to_top_atmosphere_boundary(r, mu);
77+
78+
// Minimum and maximum distance to the top atmosphere boundary from a point at height r
79+
let d_min = atmosphere.top_radius - r; // length of the ray straight up to the top atmosphere boundary
80+
let d_max = rho + H; // length of the ray to the top atmosphere boundary and grazing the horizon
81+
82+
let u = (d - d_min) / (d_max - d_min);
83+
let v = rho / H;
84+
return vec2<f32>(u, v);
85+
}
86+
87+
// Inverse of the mapping above, mapping from UV coordinates in the transmittance LUT to view height (r) and zenith cos angle (mu)
88+
fn transmittance_lut_uv_to_r_mu(uv: vec2<f32>) -> vec2<f32> {
89+
// Distance to top atmosphere boundary for a horizontal ray at ground level
90+
let H = sqrt(atmosphere.top_radius * atmosphere.top_radius - atmosphere.bottom_radius * atmosphere.bottom_radius);
91+
92+
// Distance to the horizon, from which we can compute r:
93+
let rho = H * uv.y;
94+
let r = sqrt(rho * rho + atmosphere.bottom_radius * atmosphere.bottom_radius);
95+
96+
// Distance to the top atmosphere boundary for the ray (r,mu), and its minimum
97+
// and maximum values over all mu- obtained for (r,1) and (r,mu_horizon) -
98+
// from which we can recover mu:
99+
let d_min = atmosphere.top_radius - r;
100+
let d_max = rho + H;
101+
let d = d_min + uv.x * (d_max - d_min);
102+
103+
var mu: f32;
104+
if d == 0.0 {
105+
mu = 1.0;
106+
} else {
107+
mu = (H * H - rho * rho - d * d) / (2.0 * r * d);
108+
}
109+
110+
mu = clamp(mu, -1.0, 1.0);
111+
112+
return vec2<f32>(r, mu);
113+
}
114+
115+
/// Simplified ray-sphere intersection
116+
/// where:
117+
/// Ray origin, o = [0,0,r] with r <= atmosphere.top_radius
118+
/// mu is the cosine of spherical coordinate theta (-1.0 <= mu <= 1.0)
119+
/// so ray direction in spherical coordinates is [1,acos(mu),0] which needs to be converted to cartesian
120+
/// Direction of ray, u = [0,sqrt(1-mu*mu),mu]
121+
/// Center of sphere, c = [0,0,0]
122+
/// Radius of sphere, r = atmosphere.top_radius
123+
/// This function solves the quadratic equation for line-sphere intersection simplified under these assumptions
124+
fn distance_to_top_atmosphere_boundary(r: f32, mu: f32) -> f32 {
125+
// ignore the case where r > atmosphere.top_radius
126+
let positive_discriminant = max(r * r * (mu * mu - 1.0) + atmosphere.top_radius * atmosphere.top_radius, 0.0);
127+
return max(-r * mu + sqrt(positive_discriminant), 0.0);
128+
}
129+
130+
/// Simplified ray-sphere intersection
131+
/// as above for intersections with the ground
132+
fn distance_to_bottom_atmosphere_boundary(r: f32, mu: f32) -> f32 {
133+
let positive_discriminant = max(r * r * (mu * mu - 1.0) + atmosphere.bottom_radius * atmosphere.bottom_radius, 0.0);
134+
return max(-r * mu - sqrt(positive_discriminant), 0.0);
135+
}
136+
137+
fn ray_intersects_ground(r: f32, mu: f32) -> bool {
138+
return mu < 0.0 && r * r * (mu * mu - 1.0) + atmosphere.bottom_radius * atmosphere.bottom_radius >= 0.0;
139+
}

0 commit comments

Comments
 (0)