diff --git a/crates/bevy_pbr/src/atmosphere/aerial_view_lut.wgsl b/crates/bevy_pbr/src/atmosphere/aerial_view_lut.wgsl index 0201165edaf45..c3d798305443a 100644 --- a/crates/bevy_pbr/src/atmosphere/aerial_view_lut.wgsl +++ b/crates/bevy_pbr/src/atmosphere/aerial_view_lut.wgsl @@ -4,16 +4,17 @@ types::{Atmosphere, AtmosphereSettings}, bindings::{atmosphere, settings, view, lights, aerial_view_lut_out}, functions::{ - sample_transmittance_lut, sample_atmosphere, rayleigh, henyey_greenstein, + sample_transmittance_lut, sample_density_lut, rayleigh, henyey_greenstein, sample_multiscattering_lut, AtmosphereSample, sample_local_inscattering, uv_to_ndc, max_atmosphere_distance, uv_to_ray_direction, - MIDPOINT_RATIO, get_view_position + MIDPOINT_RATIO, get_view_position, MIN_EXTINCTION, ABSORPTION_DENSITY, + SCATTERING_DENSITY, }, } } -@group(0) @binding(13) var aerial_view_lut_out: texture_storage_3d; +@group(0) @binding(16) var aerial_view_lut_out: texture_storage_3d; @compute @workgroup_size(16, 16, 1) @@ -23,7 +24,7 @@ fn main(@builtin(global_invocation_id) idx: vec3) { let uv = (vec2(idx.xy) + 0.5) / vec2(settings.aerial_view_lut_size.xy); let ray_dir = uv_to_ray_direction(uv); let world_pos = get_view_position(); - + let r = length(world_pos); let t_max = settings.aerial_view_lut_max_distance; @@ -41,15 +42,18 @@ fn main(@builtin(global_invocation_id) idx: vec3) { let local_r = length(sample_pos); let local_up = normalize(sample_pos); - let local_atmosphere = sample_atmosphere(local_r); - let sample_optical_depth = local_atmosphere.extinction * dt; + let absorption = sample_density_lut(local_r, ABSORPTION_DENSITY); + let scattering = sample_density_lut(local_r, SCATTERING_DENSITY); + let extinction = absorption + scattering; + + let sample_optical_depth = extinction * dt; let sample_transmittance = exp(-sample_optical_depth); // evaluate one segment of the integral - var inscattering = sample_local_inscattering(local_atmosphere, ray_dir, sample_pos); + var inscattering = sample_local_inscattering(scattering, ray_dir, sample_pos); // Analytical integration of the single scattering term in the radiance transfer equation - let s_int = (inscattering - inscattering * sample_transmittance) / local_atmosphere.extinction; + let s_int = (inscattering - inscattering * sample_transmittance) / max(extinction, MIN_EXTINCTION); total_inscattering += throughput * s_int; throughput *= sample_transmittance; diff --git a/crates/bevy_pbr/src/atmosphere/bindings.wgsl b/crates/bevy_pbr/src/atmosphere/bindings.wgsl index fe4e0c9070532..6a966f13980a7 100644 --- a/crates/bevy_pbr/src/atmosphere/bindings.wgsl +++ b/crates/bevy_pbr/src/atmosphere/bindings.wgsl @@ -12,11 +12,16 @@ @group(0) @binding(2) var atmosphere_transforms: AtmosphereTransforms; @group(0) @binding(3) var view: View; @group(0) @binding(4) var lights: Lights; -@group(0) @binding(5) var transmittance_lut: texture_2d; -@group(0) @binding(6) var transmittance_lut_sampler: sampler; -@group(0) @binding(7) var multiscattering_lut: texture_2d; -@group(0) @binding(8) var multiscattering_lut_sampler: sampler; -@group(0) @binding(9) var sky_view_lut: texture_2d; -@group(0) @binding(10) var sky_view_lut_sampler: sampler; -@group(0) @binding(11) var aerial_view_lut: texture_3d; -@group(0) @binding(12) var aerial_view_lut_sampler: sampler; + +@group(0) @binding(5) var medium_density_lut: texture_2d; +@group(0) @binding(6) var medium_scattering_lut: texture_2d; +@group(0) @binding(7) var medium_sampler: sampler; + +@group(0) @binding(8) var transmittance_lut: texture_2d; +@group(0) @binding(9) var transmittance_lut_sampler: sampler; +@group(0) @binding(10) var multiscattering_lut: texture_2d; +@group(0) @binding(11) var multiscattering_lut_sampler: sampler; +@group(0) @binding(12) var sky_view_lut: texture_2d; +@group(0) @binding(13) var sky_view_lut_sampler: sampler; +@group(0) @binding(14) var aerial_view_lut: texture_3d; +@group(0) @binding(15) var aerial_view_lut_sampler: sampler; diff --git a/crates/bevy_pbr/src/atmosphere/environment.rs b/crates/bevy_pbr/src/atmosphere/environment.rs index 211cce3a4b7b2..58d4e8e78baf1 100644 --- a/crates/bevy_pbr/src/atmosphere/environment.rs +++ b/crates/bevy_pbr/src/atmosphere/environment.rs @@ -1,9 +1,9 @@ use crate::{ resources::{ AtmosphereSamplers, AtmosphereTextures, AtmosphereTransform, AtmosphereTransforms, - AtmosphereTransformsOffset, + AtmosphereTransformsOffset, GpuAtmosphere, }, - GpuAtmosphereSettings, GpuLights, LightMeta, ViewLightsUniformOffset, + ExtractedAtmosphere, GpuAtmosphereSettings, GpuLights, LightMeta, ViewLightsUniformOffset, }; use bevy_asset::{load_embedded_asset, AssetServer, Assets, Handle, RenderAssetUsages}; use bevy_ecs::{ @@ -29,8 +29,6 @@ use bevy_render::{ use bevy_utils::default; use tracing::warn; -use super::Atmosphere; - // Render world representation of an environment map light for the atmosphere #[derive(Component, ExtractComponent, Clone)] pub struct AtmosphereEnvironmentMap { @@ -65,26 +63,33 @@ pub struct AtmosphereProbePipeline { pub fn init_atmosphere_probe_layout(mut commands: Commands) { let environment = BindGroupLayoutDescriptor::new( "environment_bind_group_layout", - &BindGroupLayoutEntries::sequential( + &BindGroupLayoutEntries::with_indices( ShaderStages::COMPUTE, ( - uniform_buffer::(true), - uniform_buffer::(true), - uniform_buffer::(true), - uniform_buffer::(true), - uniform_buffer::(true), - texture_2d(TextureSampleType::Float { filterable: true }), //transmittance lut and sampler - sampler(SamplerBindingType::Filtering), - texture_2d(TextureSampleType::Float { filterable: true }), //multiscattering lut and sampler - sampler(SamplerBindingType::Filtering), - texture_2d(TextureSampleType::Float { filterable: true }), //sky view lut and sampler - sampler(SamplerBindingType::Filtering), - texture_3d(TextureSampleType::Float { filterable: true }), //aerial view lut ans sampler - sampler(SamplerBindingType::Filtering), - texture_storage_2d_array( - // output 2D array texture - TextureFormat::Rgba16Float, - StorageTextureAccess::WriteOnly, + (0, uniform_buffer::(true)), + (1, uniform_buffer::(true)), + (2, uniform_buffer::(true)), + (3, uniform_buffer::(true)), + (4, uniform_buffer::(true)), + //transmittance lut and sampler + (8, texture_2d(TextureSampleType::default())), + (9, sampler(SamplerBindingType::Filtering)), + //multiscattering lut and sampler + (10, texture_2d(TextureSampleType::default())), + (11, sampler(SamplerBindingType::Filtering)), + //sky view lut and sampler + (12, texture_2d(TextureSampleType::default())), + (13, sampler(SamplerBindingType::Filtering)), + //aerial view lut ans sampler + (14, texture_3d(TextureSampleType::default())), + (15, sampler(SamplerBindingType::Filtering)), + // output 2D array texture + ( + 16, + texture_storage_2d_array( + TextureFormat::Rgba16Float, + StorageTextureAccess::WriteOnly, + ), ), ), ), @@ -101,7 +106,7 @@ pub(super) fn prepare_atmosphere_probe_bind_groups( view_uniforms: Res, lights_uniforms: Res, atmosphere_transforms: Res, - atmosphere_uniforms: Res>, + atmosphere_uniforms: Res>, settings_uniforms: Res>, pipeline_cache: Res, mut commands: Commands, @@ -110,21 +115,21 @@ pub(super) fn prepare_atmosphere_probe_bind_groups( let environment = render_device.create_bind_group( "environment_bind_group", &pipeline_cache.get_bind_group_layout(&layouts.environment), - &BindGroupEntries::sequential(( - atmosphere_uniforms.binding().unwrap(), - settings_uniforms.binding().unwrap(), - atmosphere_transforms.uniforms().binding().unwrap(), - view_uniforms.uniforms.binding().unwrap(), - lights_uniforms.view_gpu_lights.binding().unwrap(), - &textures.transmittance_lut.default_view, - &samplers.transmittance_lut, - &textures.multiscattering_lut.default_view, - &samplers.multiscattering_lut, - &textures.sky_view_lut.default_view, - &samplers.sky_view_lut, - &textures.aerial_view_lut.default_view, - &samplers.aerial_view_lut, - &textures.environment, + &BindGroupEntries::with_indices(( + (0, atmosphere_uniforms.binding().unwrap()), + (1, settings_uniforms.binding().unwrap()), + (2, atmosphere_transforms.uniforms().binding().unwrap()), + (3, view_uniforms.uniforms.binding().unwrap()), + (4, lights_uniforms.view_gpu_lights.binding().unwrap()), + (8, &textures.transmittance_lut.default_view), + (9, &samplers.transmittance_lut), + (10, &textures.multiscattering_lut.default_view), + (11, &samplers.multiscattering_lut), + (12, &textures.sky_view_lut.default_view), + (13, &samplers.sky_view_lut), + (14, &textures.aerial_view_lut.default_view), + (15, &samplers.aerial_view_lut), + (16, &textures.environment), )), ); @@ -135,7 +140,7 @@ pub(super) fn prepare_atmosphere_probe_bind_groups( } pub(super) fn prepare_probe_textures( - view_textures: Query<&AtmosphereTextures, With>, + view_textures: Query<&AtmosphereTextures, With>, probes: Query< (Entity, &AtmosphereEnvironmentMap), ( @@ -246,7 +251,7 @@ pub fn prepare_atmosphere_probe_components( pub(super) struct EnvironmentNode { main_view_query: QueryState<( - Read>, + Read>, Read>, Read, Read, diff --git a/crates/bevy_pbr/src/atmosphere/environment.wgsl b/crates/bevy_pbr/src/atmosphere/environment.wgsl index 3e96b41120f6e..efe61ae76dc66 100644 --- a/crates/bevy_pbr/src/atmosphere/environment.wgsl +++ b/crates/bevy_pbr/src/atmosphere/environment.wgsl @@ -5,7 +5,7 @@ utils::sample_cube_dir } -@group(0) @binding(13) var output: texture_storage_2d_array; +@group(0) @binding(16) var output: texture_storage_2d_array; @compute @workgroup_size(8, 8, 1) fn main(@builtin(global_invocation_id) global_id: vec3) { @@ -36,4 +36,4 @@ fn main(@builtin(global_invocation_id) global_id: vec3) { let color = vec4(inscattering, 1.0); textureStore(output, vec2(global_id.xy), i32(slice_index), color); -} \ No newline at end of file +} diff --git a/crates/bevy_pbr/src/atmosphere/functions.wgsl b/crates/bevy_pbr/src/atmosphere/functions.wgsl index e63f22d4d58cc..a48230d214ad1 100644 --- a/crates/bevy_pbr/src/atmosphere/functions.wgsl +++ b/crates/bevy_pbr/src/atmosphere/functions.wgsl @@ -7,7 +7,8 @@ bindings::{ atmosphere, settings, view, lights, transmittance_lut, transmittance_lut_sampler, multiscattering_lut, multiscattering_lut_sampler, sky_view_lut, sky_view_lut_sampler, - aerial_view_lut, aerial_view_lut_sampler, atmosphere_transforms + aerial_view_lut, aerial_view_lut_sampler, atmosphere_transforms, medium_density_lut, + medium_scattering_lut, medium_sampler, }, bruneton_functions::{ transmittance_lut_r_mu_to_uv, transmittance_lut_uv_to_r_mu, @@ -42,6 +43,7 @@ const FRAC_3_16_PI: f32 = 0.0596831036594607509; // 3 / (16π) const FRAC_4_PI: f32 = 0.07957747154594767; // 1 / (4π) const ROOT_2: f32 = 1.41421356; // √2 const EPSILON: f32 = 1.0; // 1 meter +const MIN_EXTINCTION: vec3 = vec3(1e-12); // During raymarching, each segment is sampled at a single point. This constant determines // where in the segment that sample is taken (0.0 = start, 0.5 = middle, 1.0 = end). @@ -178,72 +180,32 @@ fn sample_aerial_view_lut(uv: vec2, t: f32) -> vec3 { return exp(sample.rgb) * fade; } -// PHASE FUNCTIONS - -// -(L . V) == (L . -V). -V here is our ray direction, which points away from the view -// instead of towards it (which would be the *view direction*, V) - -// evaluates the rayleigh phase function, which describes the likelihood -// of a rayleigh scattering event scattering light from the light direction towards the view -fn rayleigh(neg_LdotV: f32) -> f32 { - return FRAC_3_16_PI * (1 + (neg_LdotV * neg_LdotV)); -} - -// evaluates the henyey-greenstein phase function, which describes the likelihood -// of a mie scattering event scattering light from the light direction towards the view -fn henyey_greenstein(neg_LdotV: f32) -> f32 { - let g = atmosphere.mie_asymmetry; - let denom = 1.0 + g * g - 2.0 * g * neg_LdotV; - return FRAC_4_PI * (1.0 - g * g) / (denom * sqrt(denom)); -} - // ATMOSPHERE SAMPLING -struct AtmosphereSample { - /// units: m^-1 - rayleigh_scattering: vec3, - - /// units: m^-1 - mie_scattering: f32, +const ABSORPTION_DENSITY: f32 = 0.0; +const SCATTERING_DENSITY: f32 = 1.0; - /// the sum of scattering and absorption. Since the phase function doesn't - /// matter for this, we combine rayleigh and mie extinction to a single - // value. - // - /// units: m^-1 - extinction: vec3 +// samples from the atmosphere density LUT. +// +// calling with `component = 0.0` will return the atmosphere's absorption density, +// while calling with `component = 1.0` will return the atmosphere's scattering density. +fn sample_density_lut(r: f32, component: f32) -> vec3 { + // sampler clamps to [0, 1] anyways, no need to clamp the altitude + let normalized_altitude = (r - atmosphere.bottom_radius) / (atmosphere.top_radius - atmosphere.bottom_radius); + let uv = vec2(1.0 - normalized_altitude, component); + return textureSampleLevel(medium_density_lut, medium_sampler, uv, 0.0).xyz; } -/// Samples atmosphere optical densities at a given radius -fn sample_atmosphere(r: f32) -> AtmosphereSample { - let altitude = clamp(r, atmosphere.bottom_radius, atmosphere.top_radius) - atmosphere.bottom_radius; - - // atmosphere values at altitude - let mie_density = exp(-atmosphere.mie_density_exp_scale * altitude); - let rayleigh_density = exp(-atmosphere.rayleigh_density_exp_scale * altitude); - var ozone_density: f32 = max(0.0, 1.0 - (abs(altitude - atmosphere.ozone_layer_altitude) / (atmosphere.ozone_layer_width * 0.5))); - - let mie_scattering = mie_density * atmosphere.mie_scattering; - let mie_absorption = mie_density * atmosphere.mie_absorption; - let mie_extinction = mie_scattering + mie_absorption; - - let rayleigh_scattering = rayleigh_density * atmosphere.rayleigh_scattering; - // no rayleigh absorption - // rayleigh extinction is the sum of scattering and absorption - - // ozone doesn't contribute to scattering - let ozone_absorption = ozone_density * atmosphere.ozone_absorption; - - var sample: AtmosphereSample; - sample.rayleigh_scattering = rayleigh_scattering; - sample.mie_scattering = mie_scattering; - sample.extinction = rayleigh_scattering + mie_extinction + ozone_absorption; - - return sample; +// samples from the atmosphere scattering LUT. `neg_LdotV` is the dot product +// of the light direction and the incoming view vector. +fn sample_scattering_lut(r: f32, neg_LdotV: f32) -> vec3 { + let normalized_altitude = (r - atmosphere.bottom_radius) / (atmosphere.top_radius - atmosphere.bottom_radius); + let uv = vec2(1.0 - normalized_altitude, neg_LdotV * 0.5 + 0.5); + return textureSampleLevel(medium_scattering_lut, medium_sampler, uv, 0.0).xyz; } /// evaluates L_scat, equation 3 in the paper, which gives the total single-order scattering towards the view at a single point -fn sample_local_inscattering(local_atmosphere: AtmosphereSample, ray_dir: vec3, world_pos: vec3) -> vec3 { +fn sample_local_inscattering(local_scattering: vec3, ray_dir: vec3, world_pos: vec3) -> vec3 { let local_r = length(world_pos); let local_up = normalize(world_pos); var inscattering = vec3(0.0); @@ -256,21 +218,16 @@ fn sample_local_inscattering(local_atmosphere: AtmosphereSample, ray_dir: vec3 RaymarchSegment { // Get both intersection points with atmosphere let atmosphere_intersections = ray_sphere_intersect(r, mu, atmosphere.top_radius); let ground_intersections = ray_sphere_intersect(r, mu, atmosphere.bottom_radius); - + var segment: RaymarchSegment; - + if r < atmosphere.bottom_radius { // Inside planet - start from bottom of atmosphere segment.start = ground_intersections.y; // Use second intersection point with ground @@ -453,26 +410,26 @@ fn raymarch_atmosphere( let r = length(pos); let up = normalize(pos); let mu = dot(ray_dir, up); - + // Optimization: Reduce sample count at close proximity to the scene let sample_count = mix(1.0, f32(max_samples), saturate(t_max * 0.01)); - + let segment = get_raymarch_segment(r, mu); let t_start = segment.start; var t_end = segment.end; - + t_end = min(t_end, t_max); let t_total = t_end - t_start; - + var result: RaymarchResult; result.inscattering = vec3(0.0); result.transmittance = vec3(1.0); - + // Skip if invalid segment if t_total <= 0.0 { return result; } - + var prev_t = t_start; var optical_depth = vec3(0.0); for (var s = 0.0; s < sample_count; s += 1.0) { @@ -484,19 +441,22 @@ fn raymarch_atmosphere( let sample_pos = pos + ray_dir * t_i; let local_r = length(sample_pos); let local_up = normalize(sample_pos); - let local_atmosphere = sample_atmosphere(local_r); - let sample_optical_depth = local_atmosphere.extinction * dt_i; + let absorption = sample_density_lut(local_r, ABSORPTION_DENSITY); + let scattering = sample_density_lut(local_r, SCATTERING_DENSITY); + let extinction = absorption + scattering; + + let sample_optical_depth = extinction * dt_i; optical_depth += sample_optical_depth; let sample_transmittance = exp(-sample_optical_depth); let inscattering = sample_local_inscattering( - local_atmosphere, + scattering, ray_dir, sample_pos ); - let s_int = (inscattering - inscattering * sample_transmittance) / local_atmosphere.extinction; + let s_int = (inscattering - inscattering * sample_transmittance) / max(extinction, MIN_EXTINCTION); result.inscattering += result.transmittance * s_int; result.transmittance *= sample_transmittance; @@ -525,4 +485,4 @@ fn raymarch_atmosphere( } return result; -} \ No newline at end of file +} diff --git a/crates/bevy_pbr/src/atmosphere/mod.rs b/crates/bevy_pbr/src/atmosphere/mod.rs index 4252134a4c1d5..acbc5ef2e3dd2 100644 --- a/crates/bevy_pbr/src/atmosphere/mod.rs +++ b/crates/bevy_pbr/src/atmosphere/mod.rs @@ -38,12 +38,13 @@ mod node; pub mod resources; use bevy_app::{App, Plugin, Update}; -use bevy_asset::embedded_asset; +use bevy_asset::{embedded_asset, AssetId, Assets, Handle}; use bevy_camera::Camera3d; use bevy_core_pipeline::core_3d::graph::Node3d; use bevy_ecs::{ component::Component, query::{Changed, QueryItem, With}, + resource::Resource, schedule::IntoScheduleConfigs, system::{lifetimeless::Read, Query}, }; @@ -71,11 +72,13 @@ use environment::{ prepare_probe_textures, AtmosphereEnvironmentMap, EnvironmentNode, }; use resources::{ - prepare_atmosphere_transforms, queue_render_sky_pipelines, AtmosphereTransforms, - RenderSkyBindGroupLayouts, + prepare_atmosphere_transforms, prepare_atmosphere_uniforms, queue_render_sky_pipelines, + AtmosphereTransforms, GpuAtmosphere, RenderSkyBindGroupLayouts, }; use tracing::warn; +use crate::medium::ScatteringMedium; + use self::{ node::{AtmosphereLutsNode, AtmosphereNode, RenderSkyNode}, resources::{ @@ -105,10 +108,16 @@ impl Plugin for AtmospherePlugin { ExtractComponentPlugin::::default(), ExtractComponentPlugin::::default(), ExtractComponentPlugin::::default(), - UniformComponentPlugin::::default(), + UniformComponentPlugin::::default(), UniformComponentPlugin::::default(), )) .add_systems(Update, prepare_atmosphere_probe_components); + + let world = app.world_mut(); + let earthlike_medium = world + .resource_mut::>() + .add(ScatteringMedium::earthlike(256, 256)); + world.insert_resource(EarthlikeAtmosphere(Atmosphere::earthlike(earthlike_medium))); } fn finish(&self, app: &mut App) { @@ -156,6 +165,9 @@ impl Plugin for AtmospherePlugin { prepare_probe_textures .in_set(RenderSystems::PrepareResources) .after(prepare_atmosphere_textures), + prepare_atmosphere_uniforms + .before(RenderSystems::PrepareResources) + .after(RenderSystems::PrepareAssets), prepare_atmosphere_probe_bind_groups.in_set(RenderSystems::PrepareBindGroups), prepare_atmosphere_transforms.in_set(RenderSystems::PrepareResources), prepare_atmosphere_bind_groups.in_set(RenderSystems::PrepareBindGroups), @@ -190,29 +202,18 @@ impl Plugin for AtmospherePlugin { } } -/// This component describes the atmosphere of a planet, and when added to a camera -/// will enable atmospheric scattering for that camera. This is only compatible with -/// HDR cameras. -/// -/// Most atmospheric particles scatter and absorb light in two main ways: -/// -/// Rayleigh scattering occurs among very small particles, like individual gas -/// molecules. It's wavelength dependent, and causes colors to separate out as -/// light travels through the atmosphere. These particles *don't* absorb light. -/// -/// Mie scattering occurs among slightly larger particles, like dust and sea spray. -/// These particles *do* absorb light, but Mie scattering and absorption is -/// *wavelength independent*. -/// -/// Ozone acts differently from the other two, and is special-cased because -/// it's very important to the look of Earth's atmosphere. It's wavelength -/// dependent, but only *absorbs* light. Also, while the density of particles -/// participating in Rayleigh and Mie scattering falls off roughly exponentially -/// from the planet's surface, ozone only exists in a band centered at a fairly -/// high altitude. -#[derive(Clone, Component, Reflect, ShaderType)] +#[derive(Resource)] +pub struct EarthlikeAtmosphere(Atmosphere); + +impl EarthlikeAtmosphere { + pub fn get(&self) -> Atmosphere { + self.0.clone() + } +} + +/// Enables atmospheric scattering for an HDR camera. +#[derive(Clone, Component)] #[require(AtmosphereSettings, Hdr)] -#[reflect(Clone, Default)] pub struct Atmosphere { /// Radius of the planet /// @@ -231,92 +232,22 @@ pub struct Atmosphere { /// units: N/A pub ground_albedo: Vec3, - /// The rate of falloff of rayleigh particulate with respect to altitude: - /// optical density = exp(-rayleigh_density_exp_scale * altitude in meters). - /// - /// THIS VALUE MUST BE POSITIVE - /// - /// units: N/A - pub rayleigh_density_exp_scale: f32, - - /// The scattering optical density of rayleigh particulate, or how - /// much light it scatters per meter - /// - /// units: m^-1 - pub rayleigh_scattering: Vec3, - - /// The rate of falloff of mie particulate with respect to altitude: - /// optical density = exp(-mie_density_exp_scale * altitude in meters) - /// - /// THIS VALUE MUST BE POSITIVE - /// - /// units: N/A - pub mie_density_exp_scale: f32, - - /// The scattering optical density of mie particulate, or how much light - /// it scatters per meter. - /// - /// units: m^-1 - pub mie_scattering: f32, - - /// The absorbing optical density of mie particulate, or how much light - /// it absorbs per meter. - /// - /// units: m^-1 - pub mie_absorption: f32, - - /// The "asymmetry" of mie scattering, or how much light tends to scatter - /// forwards, rather than backwards or to the side. - /// - /// domain: (-1, 1) - /// units: N/A - pub mie_asymmetry: f32, //the "asymmetry" value of the phase function, unitless. Domain: (-1, 1) - - /// The altitude at which the ozone layer is centered. - /// - /// units: m - pub ozone_layer_altitude: f32, - - /// The width of the ozone layer - /// - /// units: m - pub ozone_layer_width: f32, - - /// The optical density of ozone, or how much of each wavelength of - /// light it absorbs per meter. - /// - /// units: m^-1 - pub ozone_absorption: Vec3, + /// A handle to a [`ScatteringMedium`], which describes the substance + /// of the atmosphere and how it scatters light. + pub medium: Handle, } impl Atmosphere { - pub const EARTH: Atmosphere = Atmosphere { - bottom_radius: 6_360_000.0, - top_radius: 6_460_000.0, - ground_albedo: Vec3::splat(0.3), - rayleigh_density_exp_scale: 1.0 / 8_000.0, - rayleigh_scattering: Vec3::new(5.802e-6, 13.558e-6, 33.100e-6), - mie_density_exp_scale: 1.0 / 1_200.0, - mie_scattering: 3.996e-6, - mie_absorption: 0.444e-6, - mie_asymmetry: 0.8, - ozone_layer_altitude: 25_000.0, - ozone_layer_width: 30_000.0, - ozone_absorption: Vec3::new(0.650e-6, 1.881e-6, 0.085e-6), - }; - - pub fn with_density_multiplier(mut self, mult: f32) -> Self { - self.rayleigh_scattering *= mult; - self.mie_scattering *= mult; - self.mie_absorption *= mult; - self.ozone_absorption *= mult; - self - } -} - -impl Default for Atmosphere { - fn default() -> Self { - Self::EARTH + pub fn earthlike(medium: Handle) -> Self { + const EARTH_BOTTOM_RADIUS: f32 = 6_360_000.0; + const EARTH_TOP_RADIUS: f32 = 6_460_000.0; + const EARTH_ALBEDO: Vec3 = Vec3::splat(0.3); + Self { + bottom_radius: EARTH_BOTTOM_RADIUS, + top_radius: EARTH_TOP_RADIUS, + ground_albedo: EARTH_ALBEDO, + medium, + } } } @@ -325,13 +256,28 @@ impl ExtractComponent for Atmosphere { type QueryFilter = With; - type Out = Atmosphere; + type Out = ExtractedAtmosphere; fn extract_component(item: QueryItem<'_, '_, Self::QueryData>) -> Option { - Some(item.clone()) + Some(ExtractedAtmosphere { + bottom_radius: item.bottom_radius, + top_radius: item.top_radius, + ground_albedo: item.ground_albedo, + medium: item.medium.id(), + }) } } +/// The render-world representation of an `Atmosphere`, but which +/// hasn't been converted into shader uniforms yet. +#[derive(Clone, Component)] +pub struct ExtractedAtmosphere { + pub bottom_radius: f32, + pub top_radius: f32, + pub ground_albedo: Vec3, + pub medium: AssetId, +} + /// This component controls the resolution of the atmosphere LUTs, and /// how many samples are used when computing them. /// @@ -482,7 +428,7 @@ impl ExtractComponent for GpuAtmosphereSettings { } fn configure_camera_depth_usages( - mut cameras: Query<&mut Camera3d, (Changed, With)>, + mut cameras: Query<&mut Camera3d, (Changed, With)>, ) { for mut camera in &mut cameras { camera.depth_texture_usages.0 |= TextureUsages::TEXTURE_BINDING.bits(); diff --git a/crates/bevy_pbr/src/atmosphere/multiscattering_lut.wgsl b/crates/bevy_pbr/src/atmosphere/multiscattering_lut.wgsl index 2df07c98b84ec..0a89430c8a128 100644 --- a/crates/bevy_pbr/src/atmosphere/multiscattering_lut.wgsl +++ b/crates/bevy_pbr/src/atmosphere/multiscattering_lut.wgsl @@ -5,9 +5,10 @@ bindings::{atmosphere, settings}, functions::{ multiscattering_lut_uv_to_r_mu, sample_transmittance_lut, - get_local_r, get_local_up, sample_atmosphere, FRAC_4_PI, + get_local_r, get_local_up, sample_density_lut, FRAC_4_PI, max_atmosphere_distance, rayleigh, henyey_greenstein, - zenith_azimuth_to_ray_dir, + zenith_azimuth_to_ray_dir, MIN_EXTINCTION, ABSORPTION_DENSITY, + SCATTERING_DENSITY, }, bruneton_functions::{ distance_to_top_atmosphere_boundary, distance_to_bottom_atmosphere_boundary, ray_intersects_ground @@ -19,7 +20,7 @@ const PHI_2: vec2 = vec2(1.3247179572447460259609088, 1.7548776662466927600495087); -@group(0) @binding(13) var multiscattering_lut_out: texture_storage_2d; +@group(0) @binding(16) var multiscattering_lut_out: texture_storage_2d; fn s2_sequence(n: u32) -> vec2 { return fract(0.5 + f32(n) * PHI_2); @@ -100,23 +101,24 @@ fn sample_multiscattering_dir(r: f32, ray_dir: vec3, light_dir: vec3) let local_r = get_local_r(r, mu_view, t_i); let local_up = get_local_up(r, t_i, ray_dir); - let local_atmosphere = sample_atmosphere(local_r); - let sample_optical_depth = local_atmosphere.extinction * dt; + let absorption = sample_density_lut(local_r, ABSORPTION_DENSITY); + let scattering = sample_density_lut(local_r, SCATTERING_DENSITY); + let extinction = absorption + scattering; + + let sample_optical_depth = extinction * dt; let sample_transmittance = exp(-sample_optical_depth); optical_depth += sample_optical_depth; - let mu_light = dot(light_dir, local_up); - let scattering_no_phase = local_atmosphere.rayleigh_scattering + local_atmosphere.mie_scattering; - - let ms = scattering_no_phase; - let ms_int = (ms - ms * sample_transmittance) / local_atmosphere.extinction; + let ms = scattering; + let ms_int = (ms - ms * sample_transmittance) / max(extinction, MIN_EXTINCTION); f_ms += throughput * ms_int; + let mu_light = dot(light_dir, local_up); let transmittance_to_light = sample_transmittance_lut(local_r, mu_light); let shadow_factor = transmittance_to_light * f32(!ray_intersects_ground(local_r, mu_light)); - let s = scattering_no_phase * shadow_factor * FRAC_4_PI; - let s_int = (s - s * sample_transmittance) / local_atmosphere.extinction; + let s = scattering * shadow_factor * FRAC_4_PI; + let s_int = (s - s * sample_transmittance) / max(extinction, MIN_EXTINCTION); l_2 += throughput * s_int; throughput *= sample_transmittance; diff --git a/crates/bevy_pbr/src/atmosphere/node.rs b/crates/bevy_pbr/src/atmosphere/node.rs index 17b47f7d01e8b..c49debdba30f4 100644 --- a/crates/bevy_pbr/src/atmosphere/node.rs +++ b/crates/bevy_pbr/src/atmosphere/node.rs @@ -9,14 +9,14 @@ use bevy_render::{ view::{ViewTarget, ViewUniformOffset}, }; -use crate::ViewLightsUniformOffset; +use crate::{resources::GpuAtmosphere, ViewLightsUniformOffset}; use super::{ resources::{ AtmosphereBindGroups, AtmosphereLutPipelines, AtmosphereTransformsOffset, RenderSkyPipelineId, }, - Atmosphere, GpuAtmosphereSettings, + GpuAtmosphereSettings, }; #[derive(PartialEq, Eq, Debug, Copy, Clone, Hash, RenderLabel)] @@ -33,7 +33,7 @@ impl ViewNode for AtmosphereLutsNode { type ViewQuery = ( Read, Read, - Read>, + Read>, Read>, Read, Read, @@ -167,7 +167,7 @@ impl ViewNode for RenderSkyNode { type ViewQuery = ( Read, Read, - Read>, + Read>, Read>, Read, Read, diff --git a/crates/bevy_pbr/src/atmosphere/render_sky.wgsl b/crates/bevy_pbr/src/atmosphere/render_sky.wgsl index 0e0d5485c963b..8312319510432 100644 --- a/crates/bevy_pbr/src/atmosphere/render_sky.wgsl +++ b/crates/bevy_pbr/src/atmosphere/render_sky.wgsl @@ -16,9 +16,9 @@ enable dual_source_blending; #import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput #ifdef MULTISAMPLED -@group(0) @binding(13) var depth_texture: texture_depth_multisampled_2d; +@group(0) @binding(16) var depth_texture: texture_depth_multisampled_2d; #else -@group(0) @binding(13) var depth_texture: texture_depth_2d; +@group(0) @binding(16) var depth_texture: texture_depth_2d; #endif struct RenderSkyOutput { diff --git a/crates/bevy_pbr/src/atmosphere/resources.rs b/crates/bevy_pbr/src/atmosphere/resources.rs index 5c409046c0b74..162dcda36a749 100644 --- a/crates/bevy_pbr/src/atmosphere/resources.rs +++ b/crates/bevy_pbr/src/atmosphere/resources.rs @@ -1,19 +1,24 @@ -use crate::{GpuLights, LightMeta}; -use bevy_asset::{load_embedded_asset, Handle}; +use crate::{ + ExtractedAtmosphere, GpuLights, GpuScatteringMedium, LightMeta, ScatteringMedium, + ScatteringMediumSampler, +}; +use bevy_asset::{load_embedded_asset, AssetId, Handle}; use bevy_camera::{Camera, Camera3d}; use bevy_core_pipeline::FullscreenShader; use bevy_ecs::{ component::Component, entity::Entity, + error::BevyError, query::With, resource::Resource, system::{Commands, Query, Res, ResMut}, world::{FromWorld, World}, }; use bevy_image::ToExtents; -use bevy_math::{Affine3A, Mat4, Vec3A}; +use bevy_math::{Affine3A, Mat4, Vec3, Vec3A}; use bevy_render::{ extract_component::ComponentUniforms, + render_asset::RenderAssets, render_resource::{binding_types::*, *}, renderer::{RenderDevice, RenderQueue}, texture::{CachedTexture, TextureCache}, @@ -22,7 +27,7 @@ use bevy_render::{ use bevy_shader::Shader; use bevy_utils::default; -use super::{Atmosphere, GpuAtmosphereSettings}; +use super::GpuAtmosphereSettings; #[derive(Resource)] pub(crate) struct AtmosphereBindGroupLayouts { @@ -47,11 +52,15 @@ impl AtmosphereBindGroupLayouts { &BindGroupLayoutEntries::with_indices( ShaderStages::COMPUTE, ( - (0, uniform_buffer::(true)), + (0, uniform_buffer::(true)), (1, uniform_buffer::(true)), + // scattering medium luts and sampler + (5, texture_2d(TextureSampleType::default())), + (6, texture_2d(TextureSampleType::default())), + (7, sampler(SamplerBindingType::Filtering)), + // transmittance lut storage texture ( - // transmittance lut storage texture - 13, + 16, texture_storage_2d( TextureFormat::Rgba16Float, StorageTextureAccess::WriteOnly, @@ -66,13 +75,18 @@ impl AtmosphereBindGroupLayouts { &BindGroupLayoutEntries::with_indices( ShaderStages::COMPUTE, ( - (0, uniform_buffer::(true)), + (0, uniform_buffer::(true)), (1, uniform_buffer::(true)), - (5, texture_2d(TextureSampleType::Float { filterable: true })), //transmittance lut and sampler - (6, sampler(SamplerBindingType::Filtering)), + // scattering medium luts and sampler + (5, texture_2d(TextureSampleType::default())), + (6, texture_2d(TextureSampleType::default())), + (7, sampler(SamplerBindingType::Filtering)), + //transmittance lut and sampler + (8, texture_2d(TextureSampleType::default())), + (9, sampler(SamplerBindingType::Filtering)), + //multiscattering lut storage texture ( - //multiscattering lut storage texture - 13, + 16, texture_storage_2d( TextureFormat::Rgba16Float, StorageTextureAccess::WriteOnly, @@ -87,17 +101,23 @@ impl AtmosphereBindGroupLayouts { &BindGroupLayoutEntries::with_indices( ShaderStages::COMPUTE, ( - (0, uniform_buffer::(true)), + (0, uniform_buffer::(true)), (1, uniform_buffer::(true)), (2, uniform_buffer::(true)), (3, uniform_buffer::(true)), (4, uniform_buffer::(true)), - (5, texture_2d(TextureSampleType::Float { filterable: true })), //transmittance lut and sampler - (6, sampler(SamplerBindingType::Filtering)), - (7, texture_2d(TextureSampleType::Float { filterable: true })), //multiscattering lut and sampler - (8, sampler(SamplerBindingType::Filtering)), + // scattering medium luts and sampler + (5, texture_2d(TextureSampleType::default())), + (6, texture_2d(TextureSampleType::default())), + (7, sampler(SamplerBindingType::Filtering)), + //transmittance lut and sampler + (8, texture_2d(TextureSampleType::default())), + (9, sampler(SamplerBindingType::Filtering)), + //multiscattering lut and sampler + (10, texture_2d(TextureSampleType::default())), + (11, sampler(SamplerBindingType::Filtering)), ( - 13, + 16, texture_storage_2d( TextureFormat::Rgba16Float, StorageTextureAccess::WriteOnly, @@ -112,17 +132,23 @@ impl AtmosphereBindGroupLayouts { &BindGroupLayoutEntries::with_indices( ShaderStages::COMPUTE, ( - (0, uniform_buffer::(true)), + (0, uniform_buffer::(true)), (1, uniform_buffer::(true)), (3, uniform_buffer::(true)), (4, uniform_buffer::(true)), - (5, texture_2d(TextureSampleType::Float { filterable: true })), //transmittance lut and sampler - (6, sampler(SamplerBindingType::Filtering)), - (7, texture_2d(TextureSampleType::Float { filterable: true })), //multiscattering lut and sampler - (8, sampler(SamplerBindingType::Filtering)), + // scattering medium luts and sampler + (5, texture_2d(TextureSampleType::default())), + (6, texture_2d(TextureSampleType::default())), + (7, sampler(SamplerBindingType::Filtering)), + //transmittance lut and sampler + (8, texture_2d(TextureSampleType::default())), + (9, sampler(SamplerBindingType::Filtering)), + //multiscattering lut and sampler + (10, texture_2d(TextureSampleType::default())), + (11, sampler(SamplerBindingType::Filtering)), + //Aerial view lut storage texture ( - //Aerial view lut storage texture - 13, + 16, texture_storage_3d( TextureFormat::Rgba16Float, StorageTextureAccess::WriteOnly, @@ -148,28 +174,29 @@ impl FromWorld for RenderSkyBindGroupLayouts { &BindGroupLayoutEntries::with_indices( ShaderStages::FRAGMENT, ( - (0, uniform_buffer::(true)), + (0, uniform_buffer::(true)), (1, uniform_buffer::(true)), (2, uniform_buffer::(true)), (3, uniform_buffer::(true)), (4, uniform_buffer::(true)), - (5, texture_2d(TextureSampleType::Float { filterable: true })), //transmittance lut and sampler - (6, sampler(SamplerBindingType::Filtering)), - (7, texture_2d(TextureSampleType::Float { filterable: true })), //multiscattering lut and sampler - (8, sampler(SamplerBindingType::Filtering)), - (9, texture_2d(TextureSampleType::Float { filterable: true })), //sky view lut and sampler - (10, sampler(SamplerBindingType::Filtering)), - ( - // aerial view lut and sampler - 11, - texture_3d(TextureSampleType::Float { filterable: true }), - ), - (12, sampler(SamplerBindingType::Filtering)), - ( - //view depth texture - 13, - texture_2d(TextureSampleType::Depth), - ), + // scattering medium luts and sampler + (5, texture_2d(TextureSampleType::default())), + (6, texture_2d(TextureSampleType::default())), + (7, sampler(SamplerBindingType::Filtering)), + // transmittance lut and sampler + (8, texture_2d(TextureSampleType::default())), + (9, sampler(SamplerBindingType::Filtering)), + // multiscattering lut and sampler, + (10, texture_2d(TextureSampleType::default())), + (11, sampler(SamplerBindingType::Filtering)), + //sky view lut and sampler + (12, texture_2d(TextureSampleType::default())), + (13, sampler(SamplerBindingType::Filtering)), + // aerial view lut and sampler + (14, texture_3d(TextureSampleType::default())), + (15, sampler(SamplerBindingType::Filtering)), + //view depth texture + (16, texture_2d(TextureSampleType::Depth)), ), ), ); @@ -179,28 +206,29 @@ impl FromWorld for RenderSkyBindGroupLayouts { &BindGroupLayoutEntries::with_indices( ShaderStages::FRAGMENT, ( - (0, uniform_buffer::(true)), + (0, uniform_buffer::(true)), (1, uniform_buffer::(true)), (2, uniform_buffer::(true)), (3, uniform_buffer::(true)), (4, uniform_buffer::(true)), - (5, texture_2d(TextureSampleType::Float { filterable: true })), //transmittance lut and sampler - (6, sampler(SamplerBindingType::Filtering)), - (7, texture_2d(TextureSampleType::Float { filterable: true })), //multiscattering lut and sampler - (8, sampler(SamplerBindingType::Filtering)), - (9, texture_2d(TextureSampleType::Float { filterable: true })), //sky view lut and sampler - (10, sampler(SamplerBindingType::Filtering)), - ( - // aerial view lut and sampler - 11, - texture_3d(TextureSampleType::Float { filterable: true }), - ), - (12, sampler(SamplerBindingType::Filtering)), - ( - //view depth texture - 13, - texture_2d_multisampled(TextureSampleType::Depth), - ), + // scattering medium luts and sampler + (5, texture_2d(TextureSampleType::default())), + (6, texture_2d(TextureSampleType::default())), + (7, sampler(SamplerBindingType::Filtering)), + // transmittance lut and sampler + (8, texture_2d(TextureSampleType::default())), + (9, sampler(SamplerBindingType::Filtering)), + // multiscattering lut and sampler + (10, texture_2d(TextureSampleType::default())), + (11, sampler(SamplerBindingType::Filtering)), + //sky view lut and sampler + (12, texture_2d(TextureSampleType::default())), + (13, sampler(SamplerBindingType::Filtering)), + // aerial view lut and sampler + (14, texture_3d(TextureSampleType::default())), + (15, sampler(SamplerBindingType::Filtering)), + //view depth texture + (16, texture_2d_multisampled(TextureSampleType::Depth)), ), ), ); @@ -381,7 +409,7 @@ impl SpecializedRenderPipeline for RenderSkyBindGroupLayouts { } pub(super) fn queue_render_sky_pipelines( - views: Query<(Entity, &Msaa), (With, With)>, + views: Query<(Entity, &Msaa), (With, With)>, pipeline_cache: Res, layouts: Res, mut specializer: ResMut>, @@ -412,7 +440,7 @@ pub struct AtmosphereTextures { } pub(super) fn prepare_atmosphere_textures( - views: Query<(Entity, &GpuAtmosphereSettings), With>, + views: Query<(Entity, &GpuAtmosphereSettings), With>, render_device: Res, mut texture_cache: ResMut, mut commands: Commands, @@ -485,6 +513,33 @@ pub(super) fn prepare_atmosphere_textures( } } +#[derive(Copy, Clone, Debug, thiserror::Error)] +#[error("ScatteringMedium missing with id {0:?}: make sure the asset was not removed.")] +struct ScatteringMediumMissingError(AssetId); + +/// The shader-uniform representation of an Atmosphere. +#[derive(Clone, Component, ShaderType)] +pub struct GpuAtmosphere { + //TODO: rename to Planet later? + pub ground_albedo: Vec3, + pub bottom_radius: f32, + pub top_radius: f32, +} + +pub fn prepare_atmosphere_uniforms( + mut commands: Commands, + atmospheres: Query<(Entity, &ExtractedAtmosphere)>, +) -> Result<(), BevyError> { + for (entity, atmosphere) in atmospheres { + commands.entity(entity).insert(GpuAtmosphere { + ground_albedo: atmosphere.ground_albedo, + bottom_radius: atmosphere.bottom_radius, + top_radius: atmosphere.top_radius, + }); + } + Ok(()) +} + #[derive(Resource, Default)] pub struct AtmosphereTransforms { uniforms: DynamicUniformBuffer, @@ -515,7 +570,7 @@ impl AtmosphereTransformsOffset { } pub(super) fn prepare_atmosphere_transforms( - views: Query<(Entity, &ExtractedView), (With, With)>, + views: Query<(Entity, &ExtractedView), (With, With)>, render_device: Res, render_queue: Res, mut atmo_uniforms: ResMut, @@ -562,10 +617,32 @@ pub(crate) struct AtmosphereBindGroups { pub render_sky: BindGroup, } +#[derive(Copy, Clone, Debug, thiserror::Error)] +enum AtmosphereBindGroupError { + #[error("Failed to prepare atmosphere bind groups. Atmosphere uniform buffer missing")] + Atmosphere, + #[error( + "Failed to prepare atmosphere bind groups. AtmosphereTransforms uniform buffer missing" + )] + Transforms, + #[error("Failed to prepare atmosphere bind groups. AtmosphereSettings uniform buffer missing")] + Settings, + #[error("Failed to prepare atmosphere bind groups. View uniform buffer missing")] + ViewUniforms, + #[error("Failed to prepare atmosphere bind groups. Light uniform buffer missing")] + LightUniforms, +} + pub(super) fn prepare_atmosphere_bind_groups( views: Query< - (Entity, &AtmosphereTextures, &ViewDepthTexture, &Msaa), - (With, With), + ( + Entity, + &ExtractedAtmosphere, + &AtmosphereTextures, + &ViewDepthTexture, + &Msaa, + ), + (With, With), >, render_device: Res, layouts: Res, @@ -574,46 +651,55 @@ pub(super) fn prepare_atmosphere_bind_groups( view_uniforms: Res, lights_uniforms: Res, atmosphere_transforms: Res, - atmosphere_uniforms: Res>, + atmosphere_uniforms: Res>, settings_uniforms: Res>, + gpu_media: Res>, + medium_sampler: Res, pipeline_cache: Res, mut commands: Commands, -) { +) -> Result<(), BevyError> { if views.iter().len() == 0 { - return; + return Ok(()); } let atmosphere_binding = atmosphere_uniforms .binding() - .expect("Failed to prepare atmosphere bind groups. Atmosphere uniform buffer missing"); + .ok_or(AtmosphereBindGroupError::Atmosphere)?; let transforms_binding = atmosphere_transforms .uniforms() .binding() - .expect("Failed to prepare atmosphere bind groups. Atmosphere transforms buffer missing"); + .ok_or(AtmosphereBindGroupError::Transforms)?; - let settings_binding = settings_uniforms.binding().expect( - "Failed to prepare atmosphere bind groups. AtmosphereSettings uniform buffer missing", - ); + let settings_binding = settings_uniforms + .binding() + .ok_or(AtmosphereBindGroupError::Settings)?; let view_binding = view_uniforms .uniforms .binding() - .expect("Failed to prepare atmosphere bind groups. View uniform buffer missing"); + .ok_or(AtmosphereBindGroupError::ViewUniforms)?; let lights_binding = lights_uniforms .view_gpu_lights .binding() - .expect("Failed to prepare atmosphere bind groups. Lights uniform buffer missing"); + .ok_or(AtmosphereBindGroupError::LightUniforms)?; + + for (entity, atmosphere, textures, view_depth_texture, msaa) in &views { + let gpu_medium = gpu_media + .get(atmosphere.medium) + .ok_or(ScatteringMediumMissingError(atmosphere.medium))?; - for (entity, textures, view_depth_texture, msaa) in &views { let transmittance_lut = render_device.create_bind_group( "transmittance_lut_bind_group", &pipeline_cache.get_bind_group_layout(&layouts.transmittance_lut), &BindGroupEntries::with_indices(( (0, atmosphere_binding.clone()), (1, settings_binding.clone()), - (13, &textures.transmittance_lut.default_view), + (5, &gpu_medium.density_lut_view), + (6, &gpu_medium.scattering_lut_view), + (7, medium_sampler.sampler()), + (16, &textures.transmittance_lut.default_view), )), ); @@ -623,9 +709,12 @@ pub(super) fn prepare_atmosphere_bind_groups( &BindGroupEntries::with_indices(( (0, atmosphere_binding.clone()), (1, settings_binding.clone()), - (5, &textures.transmittance_lut.default_view), - (6, &samplers.transmittance_lut), - (13, &textures.multiscattering_lut.default_view), + (5, &gpu_medium.density_lut_view), + (6, &gpu_medium.scattering_lut_view), + (7, medium_sampler.sampler()), + (8, &textures.transmittance_lut.default_view), + (9, &samplers.transmittance_lut), + (16, &textures.multiscattering_lut.default_view), )), ); @@ -638,11 +727,14 @@ pub(super) fn prepare_atmosphere_bind_groups( (2, transforms_binding.clone()), (3, view_binding.clone()), (4, lights_binding.clone()), - (5, &textures.transmittance_lut.default_view), - (6, &samplers.transmittance_lut), - (7, &textures.multiscattering_lut.default_view), - (8, &samplers.multiscattering_lut), - (13, &textures.sky_view_lut.default_view), + (5, &gpu_medium.density_lut_view), + (6, &gpu_medium.scattering_lut_view), + (7, medium_sampler.sampler()), + (8, &textures.transmittance_lut.default_view), + (9, &samplers.transmittance_lut), + (10, &textures.multiscattering_lut.default_view), + (11, &samplers.multiscattering_lut), + (16, &textures.sky_view_lut.default_view), )), ); @@ -654,11 +746,14 @@ pub(super) fn prepare_atmosphere_bind_groups( (1, settings_binding.clone()), (3, view_binding.clone()), (4, lights_binding.clone()), - (5, &textures.transmittance_lut.default_view), - (6, &samplers.transmittance_lut), - (7, &textures.multiscattering_lut.default_view), - (8, &samplers.multiscattering_lut), - (13, &textures.aerial_view_lut.default_view), + (5, &gpu_medium.density_lut_view), + (6, &gpu_medium.scattering_lut_view), + (7, medium_sampler.sampler()), + (8, &textures.transmittance_lut.default_view), + (9, &samplers.transmittance_lut), + (10, &textures.multiscattering_lut.default_view), + (11, &samplers.multiscattering_lut), + (16, &textures.aerial_view_lut.default_view), )), ); @@ -675,15 +770,18 @@ pub(super) fn prepare_atmosphere_bind_groups( (2, transforms_binding.clone()), (3, view_binding.clone()), (4, lights_binding.clone()), - (5, &textures.transmittance_lut.default_view), - (6, &samplers.transmittance_lut), - (7, &textures.multiscattering_lut.default_view), - (8, &samplers.multiscattering_lut), - (9, &textures.sky_view_lut.default_view), - (10, &samplers.sky_view_lut), - (11, &textures.aerial_view_lut.default_view), - (12, &samplers.aerial_view_lut), - (13, view_depth_texture.view()), + (5, &gpu_medium.density_lut_view), + (6, &gpu_medium.scattering_lut_view), + (7, medium_sampler.sampler()), + (8, &textures.transmittance_lut.default_view), + (9, &samplers.transmittance_lut), + (10, &textures.multiscattering_lut.default_view), + (11, &samplers.multiscattering_lut), + (12, &textures.sky_view_lut.default_view), + (13, &samplers.sky_view_lut), + (14, &textures.aerial_view_lut.default_view), + (15, &samplers.aerial_view_lut), + (16, view_depth_texture.view()), )), ); @@ -695,4 +793,6 @@ pub(super) fn prepare_atmosphere_bind_groups( render_sky, }); } + + Ok(()) } diff --git a/crates/bevy_pbr/src/atmosphere/sky_view_lut.wgsl b/crates/bevy_pbr/src/atmosphere/sky_view_lut.wgsl index 5bb7b9417df98..dc8edcb13b583 100644 --- a/crates/bevy_pbr/src/atmosphere/sky_view_lut.wgsl +++ b/crates/bevy_pbr/src/atmosphere/sky_view_lut.wgsl @@ -4,11 +4,9 @@ types::{Atmosphere, AtmosphereSettings}, bindings::{atmosphere, view, settings}, functions::{ - sample_atmosphere, AtmosphereSample, - sample_local_inscattering, get_view_position, + get_view_position, raymarch_atmosphere, max_atmosphere_distance, direction_atmosphere_to_world, sky_view_lut_uv_to_zenith_azimuth, zenith_azimuth_to_ray_dir, - MIDPOINT_RATIO, raymarch_atmosphere, EPSILON }, } } @@ -19,7 +17,7 @@ } #import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput -@group(0) @binding(13) var sky_view_lut_out: texture_storage_2d; +@group(0) @binding(16) var sky_view_lut_out: texture_storage_2d; @compute @workgroup_size(16, 16, 1) diff --git a/crates/bevy_pbr/src/atmosphere/transmittance_lut.wgsl b/crates/bevy_pbr/src/atmosphere/transmittance_lut.wgsl index 233391e1c83f8..23c4a036333e6 100644 --- a/crates/bevy_pbr/src/atmosphere/transmittance_lut.wgsl +++ b/crates/bevy_pbr/src/atmosphere/transmittance_lut.wgsl @@ -1,14 +1,14 @@ #import bevy_pbr::atmosphere::{ types::{Atmosphere, AtmosphereSettings}, bindings::{settings, atmosphere}, - functions::{AtmosphereSample, sample_atmosphere, get_local_r, max_atmosphere_distance, MIDPOINT_RATIO}, + functions::{AtmosphereSample, sample_density_lut, get_local_r, max_atmosphere_distance, MIDPOINT_RATIO, ABSORPTION_DENSITY, SCATTERING_DENSITY}, bruneton_functions::{transmittance_lut_uv_to_r_mu, distance_to_bottom_atmosphere_boundary, distance_to_top_atmosphere_boundary}, } #import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput -@group(0) @binding(13) var transmittance_lut_out: texture_storage_2d; +@group(0) @binding(16) var transmittance_lut_out: texture_storage_2d; @compute @workgroup_size(16, 16, 1) @@ -38,10 +38,12 @@ fn ray_optical_depth(r: f32, mu: f32, sample_count: u32) -> vec3 { let r_i = get_local_r(r, mu, t_i); - let atmosphere_sample = sample_atmosphere(r_i); - let sample_optical_depth = atmosphere_sample.extinction * dt; - - optical_depth += sample_optical_depth; + // PERF: A possible later optimization would be to sample at `component = 0.5` + // (getting the average of the two rows) and then multiplying by 2 to find the sum. + let absorption = sample_density_lut(r_i, ABSORPTION_DENSITY); + let scattering = sample_density_lut(r_i, SCATTERING_DENSITY); + let extinction = absorption + scattering; + optical_depth += extinction * dt; } return optical_depth; diff --git a/crates/bevy_pbr/src/atmosphere/types.wgsl b/crates/bevy_pbr/src/atmosphere/types.wgsl index f9207dd7228c5..a96efe961ecb3 100644 --- a/crates/bevy_pbr/src/atmosphere/types.wgsl +++ b/crates/bevy_pbr/src/atmosphere/types.wgsl @@ -1,25 +1,11 @@ #define_import_path bevy_pbr::atmosphere::types struct Atmosphere { + ground_albedo: vec3, // Radius of the planet bottom_radius: f32, // units: m - // Radius at which we consider the atmosphere to 'end' for out calculations (from center of planet) top_radius: f32, // units: m - - ground_albedo: vec3, - - rayleigh_density_exp_scale: f32, - rayleigh_scattering: vec3, - - mie_density_exp_scale: f32, - mie_scattering: f32, // units: m^-1 - mie_absorption: f32, // units: m^-1 - mie_asymmetry: f32, // the "asymmetry" value of the phase function, unitless. Domain: (-1, 1) - - ozone_layer_altitude: f32, // units: m - ozone_layer_width: f32, // units: m - ozone_absorption: vec3, // ozone absorption. units: m^-1 } struct AtmosphereSettings { @@ -38,7 +24,6 @@ struct AtmosphereSettings { rendering_method: u32, } - // "Atmosphere space" is just the view position with y=0 and oriented horizontally, // so the horizon stays a horizontal line in our luts struct AtmosphereTransforms { diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 6eaa1f99ddf30..79163cb648134 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -36,6 +36,7 @@ mod light_probe; mod lightmap; mod material; mod material_bind_groups; +mod medium; mod mesh_material; mod parallax; mod pbr_material; @@ -62,6 +63,7 @@ pub use light_probe::*; pub use lightmap::*; pub use material::*; pub use material_bind_groups::*; +pub use medium::*; pub use mesh_material::*; pub use parallax::*; pub use pbr_material::*; @@ -247,7 +249,7 @@ impl Plugin for PbrPlugin { SyncComponentPlugin::::default(), SyncComponentPlugin::::default(), )) - .add_plugins(AtmospherePlugin) + .add_plugins((ScatteringMediumPlugin, AtmospherePlugin)) .configure_sets( PostUpdate, ( diff --git a/crates/bevy_pbr/src/medium.rs b/crates/bevy_pbr/src/medium.rs new file mode 100644 index 0000000000000..edaf145cc5f83 --- /dev/null +++ b/crates/bevy_pbr/src/medium.rs @@ -0,0 +1,555 @@ +use alloc::{borrow::Cow, sync::Arc}; +use core::f32::{self, consts::PI}; + +use bevy_app::{App, Plugin}; +use bevy_asset::{Asset, AssetApp, AssetId}; +use bevy_ecs::{ + resource::Resource, + system::{Commands, Res, SystemParamItem}, +}; +use bevy_math::{ops, Curve, FloatPow, Vec3, Vec4}; +use bevy_reflect::TypePath; +use bevy_render::{ + render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin}, + render_resource::{ + Extent3d, FilterMode, Sampler, SamplerDescriptor, Texture, TextureDataOrder, + TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, TextureView, + TextureViewDescriptor, + }, + renderer::{RenderDevice, RenderQueue}, + RenderApp, RenderStartup, +}; +use smallvec::SmallVec; + +#[doc(hidden)] +pub struct ScatteringMediumPlugin; + +impl Plugin for ScatteringMediumPlugin { + fn build(&self, app: &mut App) { + app.init_asset::() + .add_plugins(RenderAssetPlugin::::default()); + + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.add_systems(RenderStartup, init_scattering_medium_sampler); + } + } +} + +/// An asset that defines how a material scatters light. +/// +/// In order to calculate how light passes through a medium, +/// you need three pieces of information: +/// - how much light the medium *absorbs* per unit length +/// - how much light the medium *scatters* per unit length +/// - what *directions* the medium is likely to scatter light in. +/// +/// The first two are fairly simple, and are sometimes referred to together +/// (accurately enough) as the medium's [optical density]. +/// +/// The last, defined by a [phase function], is the most important in creating +/// the look of a medium. Our brains are very good at noticing (if unconsciously) +/// that a dust storm scatters light differently than a rain cloud, for example. +/// See the docs on [`PhaseFunction`] for more info. +/// +/// In reality, media are often composed of multiple elements that scatter light +/// independently, for Earth's atmosphere is composed of the gas itself, but also +/// suspended dust and particulate. These each scatter light differently, and are +/// distributed in different amounts at different altitudes. In a [`ScatteringMedium`], +/// these are each represented by a [`ScatteringTerm`] +/// +/// ## Technical Details +/// +/// A [`ScatteringMedium`] is represented on the GPU by a set of two LUTs, which +/// are re-created every time the asset is modified. See the docs on +/// [`GpuScatteringMedium`] for more info. +/// +/// [optical density]: https://en.wikipedia.org/wiki/Optical_Density +/// [phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions +#[derive(TypePath, Asset, Clone)] +pub struct ScatteringMedium { + /// An optional label for the medium, used when creating the LUTs on the GPU. + pub label: Option>, + /// The resolution at which to sample the falloff distribution of each + /// scattering term. Custom or more detailed distributions may benefit + /// from a higher value, at the cost of more memory use. + pub falloff_resolution: u32, + /// The resolution at which to sample the phase function of each scattering + /// term. Custom or more detailed phase functions may benefit from a higher + /// value, at the cost of more memory use. + pub phase_resolution: u32, + /// The list of [`ScatteringTerm`]s that compose this [`ScatteringMedium`] + pub terms: SmallVec<[ScatteringTerm; 1]>, +} + +impl ScatteringMedium { + // Returns a scattering medium with a default label and the + // specified scattering terms. + pub fn new( + falloff_resolution: u32, + phase_resolution: u32, + terms: impl IntoIterator, + ) -> Self { + Self { + label: None, + falloff_resolution, + phase_resolution, + terms: terms.into_iter().collect(), + } + } + + // Consumes and returns this scattering medium with a new label. + pub fn with_label(self, label: impl Into>) -> Self { + Self { + label: Some(label.into()), + ..self + } + } + + // Consumes and returns this scattering medium with each scattering terms' + // densities multiplied by `multiplier`. + pub fn with_density_multiplier(mut self, multiplier: f32) -> Self { + self.terms.iter_mut().for_each(|term| { + term.absorption *= multiplier; + term.scattering *= multiplier; + }); + + self + } + + /// Returns a scattering medium representing an earthlike atmosphere. + pub fn earthlike(falloff_resolution: u32, phase_resolution: u32) -> Self { + Self::new( + falloff_resolution, + phase_resolution, + [ + ScatteringTerm { + absorption: Vec3::ZERO, + scattering: Vec3::new(5.802e-6, 13.558e-6, 33.100e-6), + falloff: Falloff::Exponential { strength: 12.5 }, + phase: PhaseFunction::Rayleigh, + }, + ScatteringTerm { + absorption: Vec3::splat(3.996e-6), + scattering: Vec3::splat(0.444e-6), + falloff: Falloff::Exponential { strength: 83.5 }, + phase: PhaseFunction::Mie { asymmetry: 0.8 }, + }, + ScatteringTerm { + absorption: Vec3::new(0.650e-6, 1.881e-6, 0.085e-6), + scattering: Vec3::ZERO, + falloff: Falloff::Tent { + center: 0.75, + width: 0.3, + }, + phase: PhaseFunction::Isotropic, + }, + ], + ) + .with_label("earthlike_atmosphere") + } +} + +/// An individual element of a [`ScatteringMedium`]. +/// +/// A [`ScatteringMedium`] can be built out of a number of simpler [`ScatteringTerm`]s, +/// which correspond to an individual element of the medium. For example, Earth's +/// atmosphere would be (roughly) composed of two [`ScatteringTerm`]s: the atmospheric +/// gases themselves, which extend to the edge of space, and suspended dust particles, +/// which are denser but lie closer to the ground. +#[derive(Default, Clone)] +pub struct ScatteringTerm { + /// This term's optical obsorption density, or how much light of each wavelength + /// it absorbs per meter. + /// + /// units: m^-1 + pub absorption: Vec3, + /// This term's optical scattering density, or how much light of each wavelength + /// it scatters per meter. + /// + /// units: m^-1 + pub scattering: Vec3, + /// This term's falloff distribution. See the docs on [`Falloff`] for more info. + pub falloff: Falloff, + /// This term's [phase function], which determines the character of how it + /// scatters light. See the docs on [`PhaseFunction`] for more info. + /// + /// [phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions + pub phase: PhaseFunction, +} + +/// Describes how the media in a [`ScatteringTerm`] is distributed. +/// +/// This is closely related to the optical density values [`ScatteringTerm::absorption`] and +/// [`ScatteringTerm::scattering`]. Most media aren't the same density everywhere; +/// near the edge of space Earth's atmosphere is much less dense, and it absorbs +/// and scatters less light. +/// +/// [`Falloff`] determines how the density of a medium changes as a function of +/// an abstract "falloff parameter" `p`. `p = 1` denotes where the medium is the +/// densest, i.e. at the surface of the Earth, `p = 0` denotes where the medium +/// fades away completely, i.e. at the edge of space, and values between scale +/// linearly with distance, so `p = 0.5` would be halfway between the surface +/// and the edge of space. +/// +/// When processing a [`ScatteringMedium`], the `absorption` and `scattering` values +/// for each [`ScatteringTerm`] are multiplied by the value of the falloff function, `f(p)`. +#[derive(Default, Clone)] +pub enum Falloff { + /// A simple linear falloff function, which essentially + /// passes the falloff parameter through unchanged. + /// + /// f(1) = 1 + /// f(0) = 0 + /// f(p) = p + #[default] + Linear, + /// An exponential falloff function with adjustable strength. + /// + /// f(1) = 1 + /// f(0) = 0 + /// f(p) = (e^sp - 1)/(e^s - 1) + Exponential { + /// The "strength" of the exponential falloff. The higher + /// this value is, the quicker the medium's density will + /// decrease with distance. + /// + /// domain: (-∞, ∞) + strength: f32, + }, + /// A tent-shaped falloff function, which produces a triangular + /// peak at the center and linearly falls off to either side. + /// + /// f(`center`) = 1 + /// f(`center` +- `width` / 2) = 0 + Tent { + /// The center of the tent function peak + /// + /// domain: [0, 1] + center: f32, + /// The total width of the tent function peak + /// + /// domain: [0, 1] + width: f32, + }, + /// A falloff function defined by a custom curve. + /// + /// domain: [0, 1], + /// range: [0, 1], + Curve(Arc + Send + Sync>), +} + +impl Falloff { + /// Returns a falloff function corresponding to a custom curve. + pub fn from_curve(curve: impl Curve + Send + Sync + 'static) -> Self { + Self::Curve(Arc::new(curve)) + } + + fn sample(&self, falloff: f32) -> f32 { + match self { + Falloff::Linear => falloff, + Falloff::Exponential { strength } => { + // fill discontinuity at strength == 0 + if *strength == 0.0 { + falloff + } else { + let scale_exp_m1 = ops::exp_m1(*strength); + let domain_offset = ops::ln(scale_exp_m1.abs()); + let range_offset = scale_exp_m1.recip(); + let eval_pos = falloff * strength - domain_offset; + scale_exp_m1.signum() * ops::exp(eval_pos) - range_offset + } + } + Falloff::Tent { center, width } => { + (1.0 - (falloff - center).abs() / (0.5 * width)).max(0.0) + } + Falloff::Curve(curve) => curve.sample(falloff).unwrap_or(0.0), + } + } +} + +/// Describes how a [`ScatteringTerm`] scatters light in different directions. +/// +/// A [phase function] is a function `f: [-1, 1] -> [0, ∞)`, symmetric about `x=0` +/// whose input is the cosine of the angle between an incoming light direction and +/// and outgoing light direction, and whose output is the proportion of the incoming +/// light that is actually scattered in that direction. +/// +/// The phase function has an important effect on the "look" of a medium in a scene. +/// Media consisting of particles of a different size or shape scatter light differently, +/// and our brains are very good at telling the difference. A dust cloud, which might +/// correspond roughly to `PhaseFunction::Mie { asymmetry: 0.8 }`, looks quite different +/// from the rest of the sky (atmospheric gases), which correspond to `PhaseFunction::Rayleigh` +/// +/// [phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions +#[derive(Clone)] +pub enum PhaseFunction { + /// A phase function that scatters light evenly in all directions. + Isotropic, + + /// A phase function representing [Rayleigh scattering]. + /// + /// Rayleigh scattering occurs naturally for particles much smaller than + /// the wavelengths of visible light, such as gas molecules in the atmosphere. + /// It's generally wavelength-dependent, where shorter wavelengths are scattered + /// more strongly, so [scattering](ScatteringTerm::scattering) should have + /// higher values for blue than green and green than red. Particles that + /// participate in Rayleigh scattering don't absorb any light, either. + /// + /// [Rayleigh scattering]: https://en.wikipedia.org/wiki/Rayleigh_scattering + Rayleigh, + + /// The [Henyey-Greenstein phase function], which approximates [Mie scattering]. + /// + /// Mie scattering occurs naturally for spherical particles of dust + /// and aerosols roughly the same size as the wavelengths of visible light, + /// so it's useful for representing dust or sea spray. It's generally + /// wavelength-independent, so [absorption](ScatteringTerm::absorption) + /// and [scattering](ScatteringTerm::scattering) should be set to a greyscale value. + /// + /// [Mie scattering]: https://en.wikipedia.org/wiki/Mie_scattering + /// [Henyey-Greenstein phase function]: https://www.oceanopticsbook.info/view/scattering/level-2/the-henyey-greenstein-phase-function + Mie { + /// Whether the Mie scattering function is biased towards scattering + /// light forwards (asymmetry > 0) or backwards (asymmetry < 0). + /// + /// domain: [-1, 1] + asymmetry: f32, + }, + + /// A phase function defined by a custom curve, where the input + /// is the cosine of the angle between the incoming light ray + /// and the scattered light ray, and the output is the fraction + /// of the incoming light scattered in that direction. + /// + /// Note: it's important for photorealism that the phase function + /// be *energy conserving*, meaning that in total no more light can + /// be scattered than actually entered the medium. For this to be + /// the case, the integral of the phase function over its domain must + /// be equal to 1/2π. + /// + /// 1 + /// ∫ p(x) dx = 1/2π + /// -1 + /// + /// domain: [-1, 1] + /// range: [0, 1] + Curve(Arc + Send + Sync>), +} + +impl PhaseFunction { + /// A phase function defined by a custom curve. + pub fn from_curve(curve: impl Curve + Send + Sync + 'static) -> Self { + Self::Curve(Arc::new(curve)) + } + + fn sample(&self, neg_l_dot_v: f32) -> f32 { + const FRAC_4_PI: f32 = 0.25 / PI; + const FRAC_3_16_PI: f32 = 0.1875 / PI; + match self { + PhaseFunction::Isotropic => FRAC_4_PI, + PhaseFunction::Rayleigh => FRAC_3_16_PI * (1.0 + neg_l_dot_v * neg_l_dot_v), + PhaseFunction::Mie { asymmetry } => { + let denom = 1.0 + asymmetry.squared() - 2.0 * asymmetry * neg_l_dot_v; + FRAC_4_PI * (1.0 - asymmetry.squared()) / (denom * denom.sqrt()) + } + PhaseFunction::Curve(curve) => curve.sample(neg_l_dot_v).unwrap_or(0.0), + } + } +} + +impl Default for PhaseFunction { + fn default() -> Self { + Self::Mie { asymmetry: 0.8 } + } +} + +/// The GPU representation of a [`ScatteringMedium`]. +pub struct GpuScatteringMedium { + /// The terms of the scattering medium. + pub terms: SmallVec<[ScatteringTerm; 1]>, + /// The resolution at which to sample the falloff distribution of each + /// scattering term. + pub falloff_resolution: u32, + /// The resolution at which to sample the phase function of each + /// scattering term. + pub phase_resolution: u32, + /// The `density_lut`, a 2D `falloff_resolution x 2` LUT which contains the + /// medium's optical density with respect to the atmosphere's "falloff parameter", + /// a linear value which is 1.0 at the planet's surface and 0.0 at the edge of + /// space. The first and second rows correspond to absorption density and + /// scattering density respectively. + pub density_lut: Texture, + /// The default [`TextureView`] of the `density_lut` + pub density_lut_view: TextureView, + /// The `scattering_lut`, a 2D `falloff_resolution x phase_resolution` LUT which + /// contains the medium's scattering density multiplied by the phase function, with + /// the U axis corresponding to the falloff parameter and the V axis corresponding + /// to `neg_LdotV * 0.5 + 0.5`, where `neg_LdotV` is the dot product of the light + /// direction and the incoming view vector. + pub scattering_lut: Texture, + /// The default [`TextureView`] of the `scattering_lut` + pub scattering_lut_view: TextureView, +} + +impl RenderAsset for GpuScatteringMedium { + type SourceAsset = ScatteringMedium; + + type Param = (Res<'static, RenderDevice>, Res<'static, RenderQueue>); + + fn prepare_asset( + source_asset: Self::SourceAsset, + _asset_id: AssetId, + (render_device, render_queue): &mut SystemParamItem, + _previous_asset: Option<&Self>, + ) -> Result> { + let mut density: Vec = + Vec::with_capacity(2 * source_asset.falloff_resolution as usize); + + density.extend((0..source_asset.falloff_resolution).map(|i| { + let falloff = (i as f32 + 0.5) / source_asset.falloff_resolution as f32; + + source_asset + .terms + .iter() + .map(|term| term.absorption.extend(0.0) * term.falloff.sample(falloff)) + .sum::() + })); + + density.extend((0..source_asset.falloff_resolution).map(|i| { + let falloff = (i as f32 + 0.5) / source_asset.falloff_resolution as f32; + + source_asset + .terms + .iter() + .map(|term| term.scattering.extend(0.0) * term.falloff.sample(falloff)) + .sum::() + })); + + let mut scattering: Vec = Vec::with_capacity( + source_asset.falloff_resolution as usize * source_asset.phase_resolution as usize, + ); + + scattering.extend( + (0..source_asset.falloff_resolution * source_asset.phase_resolution).map(|raw_i| { + let i = raw_i % source_asset.phase_resolution; + let j = raw_i / source_asset.phase_resolution; + let falloff = (i as f32 + 0.5) / source_asset.falloff_resolution as f32; + let phase = (j as f32 + 0.5) / source_asset.phase_resolution as f32; + let neg_l_dot_v = phase * 2.0 - 1.0; + + source_asset + .terms + .iter() + .map(|term| { + term.scattering.extend(0.0) + * term.falloff.sample(falloff) + * term.phase.sample(neg_l_dot_v) + }) + .sum::() + }), + ); + + let density_lut = render_device.create_texture_with_data( + render_queue, + &TextureDescriptor { + label: source_asset + .label + .as_deref() + .map(|label| format!("{}_density_lut", label)) + .as_deref() + .or(Some("scattering_medium_density_lut")), + size: Extent3d { + width: source_asset.falloff_resolution, + height: 2, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba32Float, + usage: TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + TextureDataOrder::LayerMajor, + bytemuck::cast_slice(density.as_slice()), + ); + + let density_lut_view = density_lut.create_view(&TextureViewDescriptor { + label: source_asset + .label + .as_deref() + .map(|label| format!("{}_density_lut_view", label)) + .as_deref() + .or(Some("scattering_medium_density_lut_view")), + ..Default::default() + }); + + let scattering_lut = render_device.create_texture_with_data( + render_queue, + &TextureDescriptor { + label: source_asset + .label + .as_deref() + .map(|label| format!("{}_scattering_lut", label)) + .as_deref() + .or(Some("scattering_medium_scattering_lut")), + size: Extent3d { + width: source_asset.falloff_resolution, + height: source_asset.phase_resolution, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba32Float, + usage: TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + TextureDataOrder::LayerMajor, + bytemuck::cast_slice(scattering.as_slice()), + ); + + let scattering_lut_view = scattering_lut.create_view(&TextureViewDescriptor { + label: source_asset + .label + .as_deref() + .map(|label| format!("{}_scattering_lut", label)) + .as_deref() + .or(Some("scattering_medium_scattering_lut_view")), + ..Default::default() + }); + + Ok(Self { + terms: source_asset.terms, + falloff_resolution: source_asset.falloff_resolution, + phase_resolution: source_asset.phase_resolution, + density_lut, + density_lut_view, + scattering_lut, + scattering_lut_view, + }) + } +} + +/// The default sampler for all scattering media LUTs. +/// +/// Just a bilinear clamp-to-edge sampler, nothing fancy. +#[derive(Resource)] +pub struct ScatteringMediumSampler(Sampler); + +impl ScatteringMediumSampler { + pub fn sampler(&self) -> &Sampler { + &self.0 + } +} + +fn init_scattering_medium_sampler(mut commands: Commands, render_device: Res) { + let sampler = render_device.create_sampler(&SamplerDescriptor { + label: Some("scattering_medium_sampler"), + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + ..Default::default() + }); + + commands.insert_resource(ScatteringMediumSampler(sampler)); +} diff --git a/examples/3d/atmosphere.rs b/examples/3d/atmosphere.rs index ff70d34fe6fe6..ece73e5bdfaa2 100644 --- a/examples/3d/atmosphere.rs +++ b/examples/3d/atmosphere.rs @@ -6,7 +6,7 @@ use bevy::{ camera::Exposure, core_pipeline::tonemapping::Tonemapping, light::{light_consts::lux, AtmosphereEnvironmentMapLight, CascadeShadowConfigBuilder}, - pbr::{Atmosphere, AtmosphereSettings}, + pbr::{AtmosphereSettings, EarthlikeAtmosphere}, post_process::bloom::Bloom, prelude::*, }; @@ -19,12 +19,12 @@ fn main() { .run(); } -fn setup_camera_fog(mut commands: Commands) { +fn setup_camera_fog(mut commands: Commands, earth_atmosphere: Res) { commands.spawn(( Camera3d::default(), Transform::from_xyz(-1.2, 0.15, 0.0).looking_at(Vec3::Y * 0.1, Vec3::Y), - // This is the component that enables atmospheric scattering for a camera - Atmosphere::EARTH, + // get the default `Atmosphere` component + earth_atmosphere.get(), // The scene is in units of 10km, so we need to scale up the // aerial view lut distance and set the scene scale accordingly. // Most usages of this feature will not need to adjust this. diff --git a/release-content/migration-guides/generalized_atmosphere.md b/release-content/migration-guides/generalized_atmosphere.md new file mode 100644 index 0000000000000..14bcd8a621997 --- /dev/null +++ b/release-content/migration-guides/generalized_atmosphere.md @@ -0,0 +1,42 @@ +--- +title: "Generalized Atmospheric Scattering Media" +authors: ["@ecoskey"] +pull_requests: [20838] +--- + +Most of the fields on `Atmosphere` have been removed in favor of a handle +to the new `ScatteringMedium` asset. + +```diff +pub struct Atmosphere { + pub bottom_radius: f32, + pub top_radius: f32, + pub ground_albedo: Vec3, ++ pub medium: Handle, +- pub rayleigh_density_exp_scale: f32, +- pub rayleigh_scattering: Vec3, +- pub mie_density_exp_scale: f32, +- pub mie_scattering: f32, +- pub mie_absorption: f32, +- pub mie_asymmetry: f32, +- pub ozone_layer_altitude: f32, +- pub ozone_layer_width: f32, +- pub ozone_absorption: Vec3, +} +``` + +Unfortunately, this means `Atmosphere` no longer implements `Default`. Instead, +you can still access the default earthlike atmosphere through the +`EarthlikeAtmosphere` resource: + +```rust +fn setup_camera( + mut commands: Commands, + earthlike_atmosphere: Res +) { + commands.spawn(( + Camera3d, + earthlike_atmosphere.get(), + )); +} +``` diff --git a/release-content/release-notes/generalized_atmosphere.md b/release-content/release-notes/generalized_atmosphere.md new file mode 100644 index 0000000000000..fade4c07c1719 --- /dev/null +++ b/release-content/release-notes/generalized_atmosphere.md @@ -0,0 +1,74 @@ +--- +title: "Generalized Atmospheric Scattering Media" +authors: ["@ecoskey"] +pull_requests: [20838] +--- + +Until now, Bevy's atmospheric scattering system has been fast and beautiful, but +not very customizable. There's only a limited number of ways to customize the +existing parameters, which constrain the system to mostly earth-like scenes. + +Bevy 0.18 introduces a new `ScatteringMedium` asset for designing atmospheric +scattering media of all kinds: clear desert skies, foggy coastlines, and +even atmospheres of other planets! We've used Bevy's asset system to the +fullest--alongside some custom optimizations--to make sure rendering stays +fast even for complicated scattering media. + +```rust +fn setup_camera( + mut commands: Commands, + mut media: ResMut>, +) { + // Also feel free to use `ScatteringMedium::earthlike()`! + let medium = media.add(ScatteringMedium::new( + 256, + 256, + [ + ScatteringTerm { + absorption: Vec3::ZERO, + scattering: Vec3::new(5.802e-6, 13.558e-6, 33.100e-6), + falloff: Falloff::Exponential { strength: 12.5 }, + phase: PhaseFunction::Rayleigh, + }, + ScatteringTerm { + absorption: Vec3::splat(3.996e-6), + scattering: Vec3::splat(0.444e-6), + falloff: Falloff::Exponential { strength: 83.5 }, + phase: PhaseFunction::Mie { asymmetry: 0.8 }, + }, + ScatteringTerm { + absorption: Vec3::new(0.650e-6, 1.881e-6, 0.085e-6), + scattering: Vec3::ZERO, + falloff: Falloff::Tent { + center: 0.75, + width: 0.3, + }, + phase: PhaseFunction::Isotropic, + }, + ], + )); + + commands.spawn(( + Camera3d, + Atmosphere::earthlike(medium) + )); +} + +// We've provided a nice `EarthlikeAtmosphere` resource +// for the most common case :) +fn setup_camera_simple( + mut commands: Commands, + earthlike_atmosphere: Res +) { + commands.spawn(( + Camera3d, + earthlike_atmosphere.get(), + )); +} +``` + +(TODO: engine example of martian/extraterrestrial sunrise) + +Alongside this change we've also added a bunch of documentation, and links to +learn more about the technical terms used. It's definitely a complex feature +under the hood, so we're hoping to make the learning curve a little less steep :)