Skip to content

Commit 3da4116

Browse files
defuzIvan Ivashchenkoalice-i-cecileJMS55
authored
Add bevy_light::SunDisk (#20434)
# Objective * Fixes **#20425** (GI probes capture the sun disk while a `DirectionalLight` also lights the scene, so we need a way to skip the disk during probe baking). * ~~**Add `sun_disk_intensity: f32` to `AtmosphereSettings`** so you can smoothly hide, dim, or over‑brighten the sun disk without affecting the visible illumination of scene objects, while still allowing the sun’s radiance to scatter into the atmosphere even when the disk itself is hidden.~~ * Add a bevy_light::SunDisk component that acts as an add‑on to DirectionalLight. #20434 (comment) ## Testing * Ran the official `atmosphere` example on a MacBook M1 (see showcase below). * Toggled bloom on/off and tried intensities 0, 0.01, 1, and 10 — the sky looks correct and no warnings appear. * Noticed a small dark “hole” in the center of the sun disk under some settings; this artifact was present before these changes (often hidden by bloom) and probably deserves further investigation. (FIXED IN THIS PR) I’m new to WGSL/shader code — please point out anything silly! 🙂 --- ## Showcase * Scene brightness changes with intensity only because bloom amplifies the bright disk; sky illumination stays unchanged. * With bloom disabled, the disk is the only thing that changes — the rest of the scene looks identical. * Raising intensity above 1.0 doesn’t affect the scene without bloom (the disk is already 100 % white) but boosts the bloom halo when bloom is enabled. * Lowering intensity reduces the bloom halo; drop it far enough and the disk blends into the sky, which is physically plausible for very dim suns. <details> <summary><strong>1.0</strong> (bloom on) — normal</summary> <img width="1680" height="1050" alt="default 1 0" src="https://github.com/user-attachments/assets/89144e52-1159-42c6-9325-f80f986349ec" /> </details> <details> <summary><strong>0.0</strong> (bloom on) — no disk</summary> <img width="1680" height="1050" alt="no sun 0 0" src="https://github.com/user-attachments/assets/2b3842ae-26ed-45bf-9266-20189bc487a7" /> </details> <details> <summary><strong>0.01</strong> (bloom on) — low brightness, tiny bloom effect</summary> <img width="1680" height="1050" alt="0 21" src="https://github.com/user-attachments/assets/8c3943df-74b6-4422-8c9b-061e56ee33f8" /> </details> <details> <summary><strong>0.00001</strong> (bloom on) — disk blends into the sky</summary> <img width="1680" height="1050" alt="0 00001" src="https://github.com/user-attachments/assets/e1228131-41b2-4f30-b50d-ebceebc8cabb" /> </details> <details> <summary><strong>10.0</strong> (bloom on) — blinding glare</summary> <img width="1680" height="1050" alt="10" src="https://github.com/user-attachments/assets/3527c6d6-27ec-4e6f-9e9f-564a6b246bba" /> </details> <details> <summary><strong>1.0</strong> (bloom off) — normal sun with no bloom</summary> <img width="1680" height="1050" alt="1 no bloom" src="https://github.com/user-attachments/assets/b62ce660-cb65-46b1-b211-a5d330b08719" /> </details> <details> <summary><strong>10.0</strong> (bloom off) — indistinguishable from 1.0 when bloom is off</summary> <img width="1680" height="1050" alt="10 no bloom" src="https://github.com/user-attachments/assets/17ea20dd-3de7-4bb9-82a1-16f7df9a9994" /> </details> Quick question for reviewers: (not part of this PR) I’ve already experimented with parameterizing the **sun angular size** and it works well. It could serve as a cool artistic control or let you match skies for planets that aren’t Earth. Would folks be interested in a follow‑up PR? <details> <summary>4× sun size</summary> <img width="1680" height="1050" alt="4x sun" src="https://github.com/user-attachments/assets/5c39ef98-0376-48b7-b845-bfad6d87c09b" /> </details> --------- Co-authored-by: Ivan Ivashchenko <[email protected]> Co-authored-by: Alice Cecile <[email protected]> Co-authored-by: JMS55 <[email protected]>
1 parent 21dbf59 commit 3da4116

File tree

6 files changed

+74
-14
lines changed

6 files changed

+74
-14
lines changed

crates/bevy_light/src/directional_light.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,3 +240,53 @@ pub fn update_directional_light_frusta(
240240
.collect();
241241
}
242242
}
243+
244+
/// Add to a [`DirectionalLight`] to control rendering of the visible solar disk in the sky.
245+
/// Affects only the disk’s appearance, not the light’s illuminance or shadows.
246+
/// Requires a `bevy::pbr::Atmosphere` component on a [`Camera3d`](bevy_camera::Camera3d) to have any effect.
247+
///
248+
/// By default, the atmosphere is rendered with [`SunDisk::EARTH`], which approximates the
249+
/// apparent size and brightness of the Sun as seen from Earth. You can also disable the sun
250+
/// disk entirely with [`SunDisk::OFF`].
251+
#[derive(Component, Clone)]
252+
#[require(DirectionalLight)]
253+
pub struct SunDisk {
254+
/// The angular size (diameter) of the sun disk in radians, as observed from the scene.
255+
pub angular_size: f32,
256+
/// Multiplier for the brightness of the sun disk.
257+
///
258+
/// `0.0` disables the disk entirely (atmospheric scattering still occurs),
259+
/// `1.0` is the default physical intensity, and values `>1.0` overexpose it.
260+
pub intensity: f32,
261+
}
262+
263+
impl SunDisk {
264+
/// Earth-like parameters for the sun disk.
265+
///
266+
/// Uses the mean apparent size (~32 arcminutes) of the Sun at 1 AU distance
267+
/// with default intensity.
268+
pub const EARTH: SunDisk = SunDisk {
269+
angular_size: 0.00930842,
270+
intensity: 1.0,
271+
};
272+
273+
/// No visible sun disk.
274+
///
275+
/// Keeps scattering and directional light illumination, but hides the disk itself.
276+
pub const OFF: SunDisk = SunDisk {
277+
angular_size: 0.0,
278+
intensity: 0.0,
279+
};
280+
}
281+
282+
impl Default for SunDisk {
283+
fn default() -> Self {
284+
Self::EARTH
285+
}
286+
}
287+
288+
impl Default for &SunDisk {
289+
fn default() -> Self {
290+
&SunDisk::EARTH
291+
}
292+
}

crates/bevy_light/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ pub use spot_light::{
4848
mod directional_light;
4949
pub use directional_light::{
5050
update_directional_light_frusta, DirectionalLight, DirectionalLightShadowMap,
51-
DirectionalLightTexture,
51+
DirectionalLightTexture, SunDisk,
5252
};
5353

5454
/// The light prelude.

crates/bevy_pbr/src/atmosphere/functions.wgsl

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636

3737

3838
// CONSTANTS
39-
4039
const FRAC_PI: f32 = 0.3183098862; // 1 / π
4140
const FRAC_2_PI: f32 = 0.15915494309; // 1 / (2π)
4241
const FRAC_3_16_PI: f32 = 0.0596831036594607509; // 3 / (16π)
@@ -275,8 +274,6 @@ fn sample_local_inscattering(local_atmosphere: AtmosphereSample, ray_dir: vec3<f
275274
return inscattering;
276275
}
277276

278-
const SUN_ANGULAR_SIZE: f32 = 0.0174533; // angular diameter of sun in radians
279-
280277
fn sample_sun_radiance(ray_dir_ws: vec3<f32>) -> vec3<f32> {
281278
let r = view_radius();
282279
let mu_view = ray_dir_ws.y;
@@ -285,11 +282,15 @@ fn sample_sun_radiance(ray_dir_ws: vec3<f32>) -> vec3<f32> {
285282
for (var light_i: u32 = 0u; light_i < lights.n_directional_lights; light_i++) {
286283
let light = &lights.directional_lights[light_i];
287284
let neg_LdotV = dot((*light).direction_to_light, ray_dir_ws);
288-
let angle_to_sun = fast_acos(neg_LdotV);
289-
let pixel_size = fwidth(angle_to_sun);
290-
let factor = smoothstep(0.0, -pixel_size * ROOT_2, angle_to_sun - SUN_ANGULAR_SIZE * 0.5);
291-
let sun_solid_angle = (SUN_ANGULAR_SIZE * SUN_ANGULAR_SIZE) * 4.0 * FRAC_PI;
292-
sun_radiance += ((*light).color.rgb / sun_solid_angle) * factor * shadow_factor;
285+
let angle_to_sun = fast_acos(clamp(neg_LdotV, -1.0, 1.0));
286+
let w = max(0.5 * fwidth(angle_to_sun), 1e-6);
287+
let sun_angular_size = (*light).sun_disk_angular_size;
288+
let sun_intensity = (*light).sun_disk_intensity;
289+
if sun_angular_size > 0.0 && sun_intensity > 0.0 {
290+
let factor = 1 - smoothstep(sun_angular_size * 0.5 - w, sun_angular_size * 0.5 + w, angle_to_sun);
291+
let sun_solid_angle = (sun_angular_size * sun_angular_size) * 0.25 * PI;
292+
sun_radiance += ((*light).color.rgb / sun_solid_angle) * sun_intensity * factor * shadow_factor;
293+
}
293294
}
294295
return sun_radiance;
295296
}

crates/bevy_pbr/src/render/light.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use bevy_ecs::{
2222
use bevy_light::cascade::Cascade;
2323
use bevy_light::cluster::assign::{calculate_cluster_factors, ClusterableObjectType};
2424
use bevy_light::cluster::GlobalVisibleClusterableObjects;
25+
use bevy_light::SunDisk;
2526
use bevy_light::{
2627
spot_light_clip_from_view, spot_light_world_from_view, AmbientLight, CascadeShadowConfig,
2728
Cascades, DirectionalLight, DirectionalLightShadowMap, NotShadowCaster, PointLight,
@@ -103,6 +104,8 @@ pub struct ExtractedDirectionalLight {
103104
pub soft_shadow_size: Option<f32>,
104105
/// True if this light is using two-phase occlusion culling.
105106
pub occlusion_culling: bool,
107+
pub sun_disk_angular_size: f32,
108+
pub sun_disk_intensity: f32,
106109
}
107110

108111
// NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_view_types.wgsl!
@@ -138,6 +141,8 @@ pub struct GpuDirectionalLight {
138141
cascades_overlap_proportion: f32,
139142
depth_texture_base_index: u32,
140143
decal_index: u32,
144+
sun_disk_angular_size: f32,
145+
sun_disk_intensity: f32,
141146
}
142147

143148
// NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_view_types.wgsl!
@@ -327,6 +332,7 @@ pub fn extract_lights(
327332
Option<&RenderLayers>,
328333
Option<&VolumetricLight>,
329334
Has<OcclusionCulling>,
335+
Option<&SunDisk>,
330336
),
331337
Without<SpotLight>,
332338
>,
@@ -508,6 +514,7 @@ pub fn extract_lights(
508514
maybe_layers,
509515
volumetric_light,
510516
occlusion_culling,
517+
sun_disk,
511518
) in &directional_lights
512519
{
513520
if !view_visibility.get() {
@@ -574,6 +581,8 @@ pub fn extract_lights(
574581
frusta: extracted_frusta,
575582
render_layers: maybe_layers.unwrap_or_default().clone(),
576583
occlusion_culling,
584+
sun_disk_angular_size: sun_disk.unwrap_or_default().angular_size,
585+
sun_disk_intensity: sun_disk.unwrap_or_default().intensity,
577586
},
578587
RenderCascadesVisibleEntities {
579588
entities: cascade_visible_entities,
@@ -1200,6 +1209,8 @@ pub fn prepare_lights(
12001209
num_cascades: num_cascades as u32,
12011210
cascades_overlap_proportion: light.cascade_shadow_config.overlap_proportion,
12021211
depth_texture_base_index: num_directional_cascades_enabled_for_this_view as u32,
1212+
sun_disk_angular_size: light.sun_disk_angular_size,
1213+
sun_disk_intensity: light.sun_disk_intensity,
12031214
decal_index: decals
12041215
.as_ref()
12051216
.and_then(|decals| decals.get(*light_entity))

crates/bevy_pbr/src/render/mesh_view_types.wgsl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ struct DirectionalLight {
4141
cascades_overlap_proportion: f32,
4242
depth_texture_base_index: u32,
4343
decal_index: u32,
44+
sun_disk_angular_size: f32,
45+
sun_disk_intensity: f32,
4446
};
4547

4648
const DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u << 0u;

crates/bevy_solari/src/scene/binder.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,6 @@ use core::{f32::consts::TAU, hash::Hash, num::NonZeroU32, ops::Deref};
2323
const MAX_MESH_SLAB_COUNT: NonZeroU32 = NonZeroU32::new(500).unwrap();
2424
const MAX_TEXTURE_COUNT: NonZeroU32 = NonZeroU32::new(5_000).unwrap();
2525

26-
/// Average angular diameter of the sun as seen from earth.
27-
/// <https://en.wikipedia.org/wiki/Angular_diameter#Use_in_astronomy>
28-
const SUN_ANGULAR_DIAMETER_RADIANS: f32 = 0.00930842;
29-
3026
const TEXTURE_MAP_NONE: u32 = u32::MAX;
3127
const LIGHT_NOT_PRESENT_THIS_FRAME: u32 = u32::MAX;
3228

@@ -407,7 +403,7 @@ struct GpuDirectionalLight {
407403

408404
impl GpuDirectionalLight {
409405
fn new(directional_light: &ExtractedDirectionalLight) -> Self {
410-
let cos_theta_max = cos(SUN_ANGULAR_DIAMETER_RADIANS / 2.0);
406+
let cos_theta_max = cos(directional_light.sun_disk_angular_size / 2.0);
411407
let solid_angle = TAU * (1.0 - cos_theta_max);
412408
let luminance =
413409
(directional_light.color.to_vec3() * directional_light.illuminance) / solid_angle;

0 commit comments

Comments
 (0)