Web support for atmosphere (#18582)

# Objective

Add web support to atmosphere by gating dual source blending and using a
macro to determine the target platform.
The main objective of this PR is to ensure that users of Bevy's
atmosphere feature can also run it in a web-based context where WebGPU
support is enabled.

## Solution

- Make use of the `#[cfg(not(target_arch = "wasm32"))]` macro to gate
the dual source blending, as this is not (yet) supported in web
browsers.
- Rename the function `sample_sun_illuminance` to `sample_sun_radiance`
and move calls out of conditionals to ensure the shader compiles and
runs in both native and web-based contexts.
- Moved the multiplication of the transmittance out when calculating the
sun color, because calling the `sample_sun_illuminance` function was
causing issues in web. Overall this results in cleaner code and more
readable.

## Testing

- Tested by building a wasm target and loading it in a web page with
Vite dev server using `mate-h/bevy-webgpu` repo template.
- Tested the native build with `cargo run --example atmosphere` to
ensure it still works with dual source blending.

---

## Showcase

Screenshots show the atmosphere example running in two different
contexts:

<img width="1281" alt="atmosphere-web-showcase"
src="https://github.com/user-attachments/assets/40b1ee91-89ae-41a6-8189-89630d1ca1a6"
/>

---------

Co-authored-by: JMS55 <47158642+JMS55@users.noreply.github.com>
This commit is contained in:
Máté Homolya 2025-04-06 13:06:55 -07:00 committed by GitHub
parent a1fd3a4c69
commit a616ffa8ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 36 additions and 18 deletions

View File

@ -277,11 +277,11 @@ fn sample_local_inscattering(local_atmosphere: AtmosphereSample, ray_dir: vec3<f
const SUN_ANGULAR_SIZE: f32 = 0.0174533; // angular diameter of sun in radians const SUN_ANGULAR_SIZE: f32 = 0.0174533; // angular diameter of sun in radians
fn sample_sun_illuminance(ray_dir_ws: vec3<f32>, transmittance: vec3<f32>) -> vec3<f32> { fn sample_sun_radiance(ray_dir_ws: vec3<f32>) -> vec3<f32> {
let r = view_radius(); let r = view_radius();
let mu_view = ray_dir_ws.y; let mu_view = ray_dir_ws.y;
let shadow_factor = f32(!ray_intersects_ground(r, mu_view)); let shadow_factor = f32(!ray_intersects_ground(r, mu_view));
var sun_illuminance = vec3(0.0); var sun_radiance = vec3(0.0);
for (var light_i: u32 = 0u; light_i < lights.n_directional_lights; light_i++) { for (var light_i: u32 = 0u; light_i < lights.n_directional_lights; light_i++) {
let light = &lights.directional_lights[light_i]; let light = &lights.directional_lights[light_i];
let neg_LdotV = dot((*light).direction_to_light, ray_dir_ws); let neg_LdotV = dot((*light).direction_to_light, ray_dir_ws);
@ -289,9 +289,9 @@ fn sample_sun_illuminance(ray_dir_ws: vec3<f32>, transmittance: vec3<f32>) -> ve
let pixel_size = fwidth(angle_to_sun); let pixel_size = fwidth(angle_to_sun);
let factor = smoothstep(0.0, -pixel_size * ROOT_2, angle_to_sun - SUN_ANGULAR_SIZE * 0.5); let factor = smoothstep(0.0, -pixel_size * ROOT_2, angle_to_sun - SUN_ANGULAR_SIZE * 0.5);
let sun_solid_angle = (SUN_ANGULAR_SIZE * SUN_ANGULAR_SIZE) * 4.0 * FRAC_PI; let sun_solid_angle = (SUN_ANGULAR_SIZE * SUN_ANGULAR_SIZE) * 4.0 * FRAC_PI;
sun_illuminance += ((*light).color.rgb / sun_solid_angle) * factor * shadow_factor; sun_radiance += ((*light).color.rgb / sun_solid_angle) * factor * shadow_factor;
} }
return sun_illuminance * transmittance * view.exposure; return sun_radiance;
} }
// TRANSFORM UTILITIES // TRANSFORM UTILITIES

View File

@ -25,6 +25,10 @@
//! at once is untested, and might not be physically accurate. These may be //! at once is untested, and might not be physically accurate. These may be
//! integrated into a single module in the future. //! integrated into a single module in the future.
//! //!
//! On web platforms, atmosphere rendering will look slightly different. Specifically, when calculating how light travels
//! through the atmosphere, we use a simpler averaging technique instead of the more
//! complex blending operations. This difference will be resolved for WebGPU in a future release.
//!
//! [Shadertoy]: https://www.shadertoy.com/view/slSXRW //! [Shadertoy]: https://www.shadertoy.com/view/slSXRW
//! //!
//! [Unreal Engine Implementation]: https://github.com/sebh/UnrealEngineSkyAtmosphere //! [Unreal Engine Implementation]: https://github.com/sebh/UnrealEngineSkyAtmosphere
@ -46,8 +50,6 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{ use bevy_render::{
extract_component::UniformComponentPlugin, extract_component::UniformComponentPlugin,
render_resource::{DownlevelFlags, ShaderType, SpecializedRenderPipelines}, render_resource::{DownlevelFlags, ShaderType, SpecializedRenderPipelines},
renderer::RenderDevice,
settings::WgpuFeatures,
}; };
use bevy_render::{ use bevy_render::{
extract_component::{ExtractComponent, ExtractComponentPlugin}, extract_component::{ExtractComponent, ExtractComponentPlugin},
@ -159,15 +161,6 @@ impl Plugin for AtmospherePlugin {
}; };
let render_adapter = render_app.world().resource::<RenderAdapter>(); let render_adapter = render_app.world().resource::<RenderAdapter>();
let render_device = render_app.world().resource::<RenderDevice>();
if !render_device
.features()
.contains(WgpuFeatures::DUAL_SOURCE_BLENDING)
{
warn!("AtmospherePlugin not loaded. GPU lacks support for dual-source blending.");
return;
}
if !render_adapter if !render_adapter
.get_downlevel_capabilities() .get_downlevel_capabilities()

View File

@ -5,7 +5,7 @@
sample_transmittance_lut, sample_transmittance_lut_segment, sample_transmittance_lut, sample_transmittance_lut_segment,
sample_sky_view_lut, direction_world_to_atmosphere, sample_sky_view_lut, direction_world_to_atmosphere,
uv_to_ray_direction, uv_to_ndc, sample_aerial_view_lut, uv_to_ray_direction, uv_to_ndc, sample_aerial_view_lut,
view_radius, sample_sun_illuminance, ndc_to_camera_dist view_radius, sample_sun_radiance, ndc_to_camera_dist
}, },
}; };
#import bevy_render::view::View; #import bevy_render::view::View;
@ -20,7 +20,9 @@
struct RenderSkyOutput { struct RenderSkyOutput {
@location(0) inscattering: vec4<f32>, @location(0) inscattering: vec4<f32>,
#ifdef DUAL_SOURCE_BLENDING
@location(0) @second_blend_source transmittance: vec4<f32>, @location(0) @second_blend_source transmittance: vec4<f32>,
#endif
} }
@fragment @fragment
@ -33,15 +35,24 @@ fn main(in: FullscreenVertexOutput) -> RenderSkyOutput {
var transmittance: vec3<f32>; var transmittance: vec3<f32>;
var inscattering: vec3<f32>; var inscattering: vec3<f32>;
let sun_radiance = sample_sun_radiance(ray_dir_ws.xyz);
if depth == 0.0 { if depth == 0.0 {
let ray_dir_as = direction_world_to_atmosphere(ray_dir_ws.xyz); let ray_dir_as = direction_world_to_atmosphere(ray_dir_ws.xyz);
transmittance = sample_transmittance_lut(r, mu); transmittance = sample_transmittance_lut(r, mu);
inscattering += sample_sky_view_lut(r, ray_dir_as); inscattering += sample_sky_view_lut(r, ray_dir_as);
inscattering += sample_sun_illuminance(ray_dir_ws.xyz, transmittance); inscattering += sun_radiance * transmittance * view.exposure;
} else { } else {
let t = ndc_to_camera_dist(vec3(uv_to_ndc(in.uv), depth)); let t = ndc_to_camera_dist(vec3(uv_to_ndc(in.uv), depth));
inscattering = sample_aerial_view_lut(in.uv, t); inscattering = sample_aerial_view_lut(in.uv, t);
transmittance = sample_transmittance_lut_segment(r, mu, t); transmittance = sample_transmittance_lut_segment(r, mu, t);
} }
#ifdef DUAL_SOURCE_BLENDING
return RenderSkyOutput(vec4(inscattering, 0.0), vec4(transmittance, 1.0)); return RenderSkyOutput(vec4(inscattering, 0.0), vec4(transmittance, 1.0));
#else
let mean_transmittance = (transmittance.r + transmittance.g + transmittance.b) / 3.0;
return RenderSkyOutput(vec4(inscattering, mean_transmittance));
#endif
} }

View File

@ -326,6 +326,7 @@ pub(crate) struct RenderSkyPipelineId(pub CachedRenderPipelineId);
pub(crate) struct RenderSkyPipelineKey { pub(crate) struct RenderSkyPipelineKey {
pub msaa_samples: u32, pub msaa_samples: u32,
pub hdr: bool, pub hdr: bool,
pub dual_source_blending: bool,
} }
impl SpecializedRenderPipeline for RenderSkyBindGroupLayouts { impl SpecializedRenderPipeline for RenderSkyBindGroupLayouts {
@ -340,6 +341,15 @@ impl SpecializedRenderPipeline for RenderSkyBindGroupLayouts {
if key.hdr { if key.hdr {
shader_defs.push("TONEMAP_IN_SHADER".into()); shader_defs.push("TONEMAP_IN_SHADER".into());
} }
if key.dual_source_blending {
shader_defs.push("DUAL_SOURCE_BLENDING".into());
}
let dst_factor = if key.dual_source_blending {
BlendFactor::Src1
} else {
BlendFactor::SrcAlpha
};
RenderPipelineDescriptor { RenderPipelineDescriptor {
label: Some(format!("render_sky_pipeline_{}", key.msaa_samples).into()), label: Some(format!("render_sky_pipeline_{}", key.msaa_samples).into()),
@ -367,7 +377,7 @@ impl SpecializedRenderPipeline for RenderSkyBindGroupLayouts {
blend: Some(BlendState { blend: Some(BlendState {
color: BlendComponent { color: BlendComponent {
src_factor: BlendFactor::One, src_factor: BlendFactor::One,
dst_factor: BlendFactor::Src1, dst_factor,
operation: BlendOperation::Add, operation: BlendOperation::Add,
}, },
alpha: BlendComponent { alpha: BlendComponent {
@ -388,6 +398,7 @@ pub(super) fn queue_render_sky_pipelines(
pipeline_cache: Res<PipelineCache>, pipeline_cache: Res<PipelineCache>,
layouts: Res<RenderSkyBindGroupLayouts>, layouts: Res<RenderSkyBindGroupLayouts>,
mut specializer: ResMut<SpecializedRenderPipelines<RenderSkyBindGroupLayouts>>, mut specializer: ResMut<SpecializedRenderPipelines<RenderSkyBindGroupLayouts>>,
render_device: Res<RenderDevice>,
mut commands: Commands, mut commands: Commands,
) { ) {
for (entity, camera, msaa) in &views { for (entity, camera, msaa) in &views {
@ -397,6 +408,9 @@ pub(super) fn queue_render_sky_pipelines(
RenderSkyPipelineKey { RenderSkyPipelineKey {
msaa_samples: msaa.samples(), msaa_samples: msaa.samples(),
hdr: camera.hdr, hdr: camera.hdr,
dual_source_blending: render_device
.features()
.contains(WgpuFeatures::DUAL_SOURCE_BLENDING),
}, },
); );
commands.entity(entity).insert(RenderSkyPipelineId(id)); commands.entity(entity).insert(RenderSkyPipelineId(id));