diff --git a/crates/bevy_pbr/src/atmosphere/aerial_view_lut.wgsl b/crates/bevy_pbr/src/atmosphere/aerial_view_lut.wgsl index 9a683eddd50bd..bee67a6b68cb9 100644 --- a/crates/bevy_pbr/src/atmosphere/aerial_view_lut.wgsl +++ b/crates/bevy_pbr/src/atmosphere/aerial_view_lut.wgsl @@ -21,7 +21,9 @@ fn main(@builtin(global_invocation_id) idx: vec3) { if any(idx.xy > settings.aerial_view_lut_size.xy) { return; } - let uv = (vec2(idx.xy) + 0.5) / vec2(settings.aerial_view_lut_size.xy); + // Use global invocation ID as pixel coordinates for jittering + let pixel_coords = vec2(idx.xy); + let uv = (pixel_coords + 0.5) / vec2(settings.aerial_view_lut_size.xy); let ray_dir = uv_to_ray_direction(uv); let world_pos = get_view_position(); @@ -50,7 +52,7 @@ fn main(@builtin(global_invocation_id) idx: vec3) { let sample_transmittance = exp(-sample_optical_depth); // evaluate one segment of the integral - var inscattering = sample_local_inscattering(scattering, ray_dir, sample_pos); + var inscattering = sample_local_inscattering(scattering, ray_dir, sample_pos, pixel_coords); // Analytical integration of the single scattering term in the radiance transfer equation let s_int = (inscattering - inscattering * sample_transmittance) / max(extinction, MIN_EXTINCTION); diff --git a/crates/bevy_pbr/src/atmosphere/clouds.wgsl b/crates/bevy_pbr/src/atmosphere/clouds.wgsl new file mode 100644 index 0000000000000..5bdcfc0de27fc --- /dev/null +++ b/crates/bevy_pbr/src/atmosphere/clouds.wgsl @@ -0,0 +1,297 @@ +// Cloud rendering functions using 3D FBM noise +#define_import_path bevy_pbr::atmosphere::clouds + +#import bevy_render::maths::ray_sphere_intersect +#import bevy_pbr::utils::interleaved_gradient_noise +#import bevy_pbr::atmosphere::{ + types::{Atmosphere, AtmosphereSettings}, + bindings::{settings, atmosphere}, + functions::{ + get_local_r, + }, +} + +struct CloudLayer { + cloud_layer_start: f32, + cloud_layer_end: f32, + cloud_density: f32, + cloud_absorption: f32, + cloud_scattering: f32, + noise_scale: f32, + noise_offset: vec3, +} + +@group(0) @binding(14) var cloud_layer: CloudLayer; +@group(0) @binding(15) var noise_texture_3d: texture_3d; +@group(0) @binding(16) var noise_sampler_3d: sampler; + +/// Sample the 3D noise texture at a world position +fn sample_cloud_noise(world_pos: vec3) -> f32 { + // Convert world position to noise texture coordinates + let noise_coords = (world_pos + cloud_layer.noise_offset) / cloud_layer.noise_scale; + + // Sample the 3D noise texture with wrapping + return textureSampleLevel(noise_texture_3d, noise_sampler_3d, noise_coords, 0.0).r; +} + +/// Get cloud scattering coefficient per unit density +fn get_cloud_scattering_coeff() -> f32 { + return cloud_layer.cloud_scattering; +} + +/// Get cloud absorption coefficient per unit density +fn get_cloud_absorption_coeff() -> f32 { + return cloud_layer.cloud_absorption; +} + +/// Get cloud density at a given position (in local atmosphere space) +fn get_cloud_density(r: f32, world_pos: vec3) -> f32 { + // Check if we're within the cloud layer + if (r < cloud_layer.cloud_layer_start || r > cloud_layer.cloud_layer_end) { + return 0.0; + } + + // Calculate height factor within cloud layer (0 at bottom, 1 at top) + let layer_thickness = cloud_layer.cloud_layer_end - cloud_layer.cloud_layer_start; + let height_in_layer = r - cloud_layer.cloud_layer_start; + let height_factor = height_in_layer / layer_thickness; + + // Sample noise + var noise_value = sample_cloud_noise(world_pos); + noise_value = clamp(pow(noise_value, 3.0), 0.0, 1.0); + + // Apply contrast remapping to create sharper cloud boundaries + // This creates more contrast between cloud/no-cloud areas by remapping mid-values + let contrast_threshold = 0.3; // Controls how much contrast (lower = sharper edges, range: 0.0-0.5) + noise_value = smoothstep(contrast_threshold, 1.0 - contrast_threshold, noise_value); + + // Height-based density falloff (clouds denser in middle of layer) + let height_gradient = 1.0 - abs(height_factor * 2.0 - 1.0); + let height_multiplier = smoothstep(0.0, 0.3, height_gradient) * smoothstep(1.0, 0.6, height_gradient); + + // Combine noise with height gradient + // Density is normalized to [0, 1] for physically correct scattering coefficients + // where coefficients represent values per unit density + let density = noise_value * height_multiplier; + + return clamp(density, 0.0, 1.0); +} + +struct CloudSample { + density: f32, + scattering: f32, + absorption: f32, +} + +/// Ray march through the cloud layer +fn raymarch_clouds( + ray_origin: vec3, + ray_dir: vec3, + max_distance: f32, + steps: u32, + pixel_coords: vec2, +) -> vec4 { + // Early exit if clouds are disabled (density is 0) + if (cloud_layer.cloud_density <= 0.0) { + return vec4(0.0); + } + + let r = length(ray_origin); + let mu = dot(ray_dir, normalize(ray_origin)); + + // Find intersection with cloud layer spheres + // ray_sphere_intersect returns vec2(near_t, far_t) + let cloud_bottom_intersect = ray_sphere_intersect(r, mu, cloud_layer.cloud_layer_start); + let cloud_top_intersect = ray_sphere_intersect(r, mu, cloud_layer.cloud_layer_end); + + // Determine ray march bounds through the cloud layer + var march_start = 0.0; + var march_end = max_distance; + + if (r < cloud_layer.cloud_layer_start) { + // Below cloud layer - march from cloud bottom to cloud top + if (cloud_bottom_intersect.y < 0.0) { + return vec4(0.0); // Ray doesn't hit cloud layer + } + march_start = max(0.0, cloud_bottom_intersect.y); + march_end = min(max_distance, cloud_top_intersect.y); + } else if (r < cloud_layer.cloud_layer_end) { + // Inside cloud layer + march_start = 0.0; + march_end = min(max_distance, select(cloud_top_intersect.y, cloud_bottom_intersect.x, mu < 0.0)); + } else { + // Above cloud layer - march from cloud top to cloud bottom + if (cloud_top_intersect.x < 0.0) { + return vec4(0.0); // Ray doesn't hit cloud layer + } + march_start = max(0.0, cloud_top_intersect.x); + march_end = min(max_distance, cloud_bottom_intersect.x); + } + + if (march_start >= march_end) { + return vec4(0.0); + } + + let march_distance = march_end - march_start; + let step_size = march_distance / f32(steps); + + var cloud_color = vec3(0.0); + var transmittance = 1.0; + + // Generate noise offset for temporal jittering (reduces banding) + let jitter = interleaved_gradient_noise(pixel_coords, 0u); + + // Ray march through cloud layer + for (var i = 0u; i < steps; i++) { + if (transmittance < 0.01) { + break; + } + + // Add jitter to sample position to reduce banding artifacts + let t = march_start + (f32(i) + jitter) * step_size; + let sample_pos = ray_origin + ray_dir * t; + let r = length(sample_pos); + + let density = get_cloud_density(r, sample_pos); + + if (density > 0.01) { + // Physically correct coefficients (units: m^-1 per unit density) + // Density is normalized [0, 1], coefficients represent actual physical values + let extinction = density * (cloud_layer.cloud_scattering + cloud_layer.cloud_absorption); + let scattering = density * cloud_layer.cloud_scattering; + + // Beer's law for transmittance + let sample_transmittance = exp(-extinction * step_size); + + // Simple lighting (could be improved with light ray marching) + let light_energy = 1.0; // Simplified - should sample actual lighting + + // In-scattering contribution + // Use safe division to avoid divide-by-zero + if (extinction > 0.0001) { + cloud_color += light_energy * scattering * transmittance * (1.0 - sample_transmittance) / extinction; + } + + // Update transmittance + transmittance *= sample_transmittance; + } + } + + return vec4(cloud_color, 1.0 - transmittance); +} + +/// Raymarch through clouds towards the sun to compute volumetric shadow +/// Returns the light transmittance factor [0,1] where 0 = fully shadowed, 1 = no shadow +/// Properly handles viewer inside clouds and grazing angles +fn compute_cloud_shadow( + world_pos: vec3, + sun_dir: vec3, + steps: u32, + pixel_coords: vec2, +) -> f32 { + // Early exit if clouds are disabled + if (cloud_layer.cloud_density <= 0.0) { + return 1.0; + } + + let r = length(world_pos); + let up = normalize(world_pos); + let mu = dot(sun_dir, up); + + // Find intersection with cloud layer spheres in sun direction + let cloud_bottom_intersect = ray_sphere_intersect(r, mu, cloud_layer.cloud_layer_start); + let cloud_top_intersect = ray_sphere_intersect(r, mu, cloud_layer.cloud_layer_end); + + // Determine actual march bounds through cloud layer toward sun + var march_start = 0.0; + var march_end = 0.0; + var valid_intersection = false; + + if (r < cloud_layer.cloud_layer_start) { + // Below clouds - march from cloud bottom to top + if (cloud_bottom_intersect.y > 0.0 && cloud_top_intersect.y > cloud_bottom_intersect.y) { + march_start = cloud_bottom_intersect.y; + march_end = cloud_top_intersect.y; + valid_intersection = true; + } + } else if (r < cloud_layer.cloud_layer_end) { + // Inside cloud layer - march to exit boundary in sun direction + if (mu >= 0.0) { + // Ray going upward/outward - exit at top + if (cloud_top_intersect.y > 0.0) { + march_start = 0.0; + march_end = cloud_top_intersect.y; + valid_intersection = true; + } + } else { + // Ray going downward/inward - exit at bottom + if (cloud_bottom_intersect.x > 0.0) { + march_start = 0.0; + march_end = cloud_bottom_intersect.x; + valid_intersection = true; + } + } + } else { + // Above clouds - march from cloud top to bottom (backward along ray) + if (cloud_top_intersect.x > 0.0 && cloud_bottom_intersect.x > cloud_top_intersect.x) { + march_start = cloud_top_intersect.x; + march_end = cloud_bottom_intersect.x; + valid_intersection = true; + } + } + + if (!valid_intersection || march_start >= march_end || march_end <= 0.0) { + return 1.0; + } + + let march_distance = march_end - march_start; + let step_size = march_distance / f32(steps); + let jitter = interleaved_gradient_noise(pixel_coords, 1u); + + var optical_depth = 0.0; + + // Raymarch through clouds towards sun + for (var i = 0u; i < steps; i++) { + let t = march_start + (f32(i) + jitter) * step_size; + let sample_pos = world_pos + sun_dir * t; + let sample_r = length(sample_pos); + + // Sample density (get_cloud_density already handles bounds) + let density = get_cloud_density(sample_r, sample_pos); + + if (density > 0.01) { + // Physically correct coefficients (units: m^-1 per unit density) + let extinction = density * (cloud_layer.cloud_scattering + cloud_layer.cloud_absorption); + optical_depth += extinction * step_size; + } + } + + // Beer-Lambert law for shadow transmission + return exp(-optical_depth); +} + +/// Simplified cloud contribution for a single sample point +/// Returns (luminance_added, transmittance_multiplier) +fn sample_cloud_contribution( + world_pos: vec3, + step_size: f32, +) -> vec2 { + let r = length(world_pos); + let density = get_cloud_density(r, world_pos); + + if (density < 0.01) { + return vec2(0.0, 1.0); + } + + // Physically correct coefficients (units: m^-1 per unit density) + let extinction = density * (cloud_layer.cloud_scattering + cloud_layer.cloud_absorption); + let scattering = density * cloud_layer.cloud_scattering; + + // Beer's law + let transmittance = exp(-extinction * step_size); + + // Simple uniform scattering (could be enhanced with actual sun direction) + let in_scatter = scattering * (1.0 - transmittance); + + return vec2(in_scatter, transmittance); +} diff --git a/crates/bevy_pbr/src/atmosphere/fbm_noise.rs b/crates/bevy_pbr/src/atmosphere/fbm_noise.rs new file mode 100644 index 0000000000000..80e8a2f1cca88 --- /dev/null +++ b/crates/bevy_pbr/src/atmosphere/fbm_noise.rs @@ -0,0 +1,266 @@ +//! 3D FBM Noise Generation for Atmospheric Effects +//! +//! This module generates a 3D texture filled with Fractional Brownian Motion (FBM) noise, +//! which can be used for atmospheric effects like clouds, fog, and volumetric effects. + +use bevy_asset::load_embedded_asset; +use bevy_ecs::{ + resource::Resource, + system::{Res, ResMut}, + world::{FromWorld, World}, +}; +use bevy_math::UVec3; +use bevy_render::{ + render_resource::{binding_types::*, *}, + renderer::{RenderDevice, RenderQueue}, + texture::{CachedTexture, TextureCache}, +}; +use bevy_utils::default; + +/// Parameters for controlling FBM noise generation +#[derive(Clone, Copy, ShaderType, bytemuck::Pod, bytemuck::Zeroable)] +#[repr(C)] +pub struct FbmNoiseParams { + /// Number of octaves for the FBM noise (more octaves = more detail) + pub octaves: u32, + /// Base frequency of the noise + pub frequency: f32, + /// Base amplitude of the noise + pub amplitude: f32, + /// Frequency multiplier for each octave (typically 2.0) + pub lacunarity: f32, + /// Amplitude multiplier for each octave (typically 0.5) + pub persistence: f32, +} + +impl Default for FbmNoiseParams { + fn default() -> Self { + Self { + octaves: 4, + frequency: 1.0, + amplitude: 1.0, + lacunarity: 2.0, + persistence: 0.5, + } + } +} + +/// Size of the 3D noise texture +#[derive(Clone, Copy)] +pub struct NoiseTextureSize { + pub size: UVec3, +} + +impl Default for NoiseTextureSize { + fn default() -> Self { + Self { + size: UVec3::new(128, 128, 128), + } + } +} + +/// Resource containing the bind group layout for the FBM noise pass +#[derive(Resource)] +pub struct FbmNoiseBindGroupLayout { + pub layout: BindGroupLayout, + pub descriptor: BindGroupLayoutDescriptor, +} + +impl FromWorld for FbmNoiseBindGroupLayout { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + let descriptor = BindGroupLayoutDescriptor::new( + "fbm_noise_bind_group_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::COMPUTE, + ( + ( + // 3D noise texture storage + 13, + texture_storage_3d( + TextureFormat::Rgba16Float, + StorageTextureAccess::WriteOnly, + ), + ), + ( + // FBM parameters uniform buffer + 14, + uniform_buffer::(false), + ), + ), + ), + ); + + let layout = + render_device.create_bind_group_layout(descriptor.label.as_ref(), &descriptor.entries); + + Self { layout, descriptor } + } +} + +/// Resource containing the compute pipeline for FBM noise generation +#[derive(Resource)] +pub struct FbmNoisePipeline { + pub pipeline: CachedComputePipelineId, +} + +impl FromWorld for FbmNoisePipeline { + fn from_world(world: &mut World) -> Self { + let pipeline_cache = world.resource::(); + let layout = world.resource::(); + + let pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("fbm_noise_3d_pipeline".into()), + layout: vec![layout.descriptor.clone()], + shader: load_embedded_asset!(world, "fbm_noise_3d.wgsl"), + ..default() + }); + + Self { pipeline } + } +} + +/// Resource containing the generated 3D noise texture +#[derive(Resource)] +pub struct FbmNoiseTexture { + pub texture: CachedTexture, + pub size: UVec3, +} + +/// Resource containing the bind group for the noise generation pass +#[derive(Resource)] +pub struct FbmNoiseBindGroup { + pub bind_group: BindGroup, +} + +/// Resource containing the uniform buffer for FBM parameters +#[derive(Resource)] +pub struct FbmNoiseParamsBuffer { + pub buffer: Buffer, +} + +/// System to initialize the FBM noise texture +pub fn init_fbm_noise_texture( + mut texture_cache: ResMut, + render_device: Res, + mut commands: bevy_ecs::system::Commands, +) { + let size = NoiseTextureSize::default(); + + let texture_descriptor = TextureDescriptor { + label: Some("fbm_noise_3d_texture"), + size: Extent3d { + width: size.size.x, + height: size.size.y, + depth_or_array_layers: size.size.z, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D3, + format: TextureFormat::Rgba16Float, + usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }; + + let texture = texture_cache.get(&render_device, texture_descriptor); + + commands.insert_resource(FbmNoiseTexture { + texture, + size: size.size, + }); +} + +/// System to prepare the FBM noise bind group +pub fn prepare_fbm_noise_bind_group( + mut commands: bevy_ecs::system::Commands, + render_device: Res, + layout: Res, + texture: Res, + params_buffer: Res, +) { + let bind_group = render_device.create_bind_group( + "fbm_noise_bind_group", + &layout.layout, + &BindGroupEntries::with_indices(( + (13, &texture.texture.default_view), + (14, params_buffer.buffer.as_entire_binding()), + )), + ); + + commands.insert_resource(FbmNoiseBindGroup { bind_group }); +} + +/// System to initialize the FBM noise parameters buffer +pub fn init_fbm_noise_params_buffer( + mut commands: bevy_ecs::system::Commands, + render_device: Res, +) { + let params = FbmNoiseParams::default(); + let buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("fbm_noise_params_buffer"), + contents: bytemuck::cast_slice(&[params]), + usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, + }); + + commands.insert_resource(FbmNoiseParamsBuffer { buffer }); +} + +/// Returns the dispatch workgroup counts for the 3D noise texture based on its size +pub fn get_noise_dispatch_size(texture_size: UVec3) -> UVec3 { + const WORKGROUP_SIZE: u32 = 4; + UVec3::new( + texture_size.x.div_ceil(WORKGROUP_SIZE), + texture_size.y.div_ceil(WORKGROUP_SIZE), + texture_size.z.div_ceil(WORKGROUP_SIZE), + ) +} + +/// Resource to track if noise has been generated +#[derive(Resource, Default)] +pub struct NoiseGenerated(pub bool); + +/// System to generate the FBM noise texture (runs once) +pub fn generate_fbm_noise_once( + render_device: Res, + render_queue: Res, + pipeline_cache: Res, + pipeline: Res, + bind_group: Option>, + texture: Res, + mut noise_generated: ResMut, +) { + // Only generate once + if noise_generated.0 { + return; + } + + let Some(bind_group) = bind_group else { + return; + }; + + let Some(compute_pipeline) = pipeline_cache.get_compute_pipeline(pipeline.pipeline) else { + return; + }; + + let mut encoder = render_device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("fbm_noise_generation"), + }); + + { + let mut compute_pass = encoder.begin_compute_pass(&ComputePassDescriptor { + label: Some("fbm_noise_3d_pass"), + timestamp_writes: None, + }); + + compute_pass.set_pipeline(compute_pipeline); + compute_pass.set_bind_group(0, &bind_group.bind_group, &[]); + + let dispatch_size = get_noise_dispatch_size(texture.size); + compute_pass.dispatch_workgroups(dispatch_size.x, dispatch_size.y, dispatch_size.z); + } + + let command_buffer = encoder.finish(); + render_queue.submit([command_buffer]); + + noise_generated.0 = true; +} diff --git a/crates/bevy_pbr/src/atmosphere/fbm_noise_3d.wgsl b/crates/bevy_pbr/src/atmosphere/fbm_noise_3d.wgsl new file mode 100644 index 0000000000000..9a5143920da9d --- /dev/null +++ b/crates/bevy_pbr/src/atmosphere/fbm_noise_3d.wgsl @@ -0,0 +1,137 @@ +// 3D FBM (Fractional Brownian Motion) Noise Generator +// Generates a 3D noise texture for atmospheric effects + +#import bevy_pbr::atmosphere::{ + types::AtmosphereSettings, + bindings::settings, +} + +@group(0) @binding(13) var noise_texture_out: texture_storage_3d; + +// Parameters for FBM noise generation +struct FbmParams { + octaves: u32, + frequency: f32, + amplitude: f32, + lacunarity: f32, + persistence: f32, +} + +@group(0) @binding(14) var fbm_params: FbmParams; + +@compute +@workgroup_size(4, 4, 4) +fn main(@builtin(global_invocation_id) idx: vec3) { + let texture_size = textureDimensions(noise_texture_out); + + if (idx.x >= texture_size.x || idx.y >= texture_size.y || idx.z >= texture_size.z) { + return; + } + + // Normalized coordinates [0, 1] + let uvw = (vec3(idx) + 0.5) / vec3(texture_size); + + // Generate FBM noise + let noise_value = fbm_3d(uvw * fbm_params.frequency); + + // Store the noise value in all channels (can be used for different purposes) + textureStore(noise_texture_out, idx, vec4(noise_value, noise_value, noise_value, 1.0)); +} + +// 3D FBM noise function +fn fbm_3d(p: vec3) -> f32 { + var value = 0.0; + var amplitude = fbm_params.amplitude; + var frequency = 1.0; + var position = p; + + for (var i = 0u; i < fbm_params.octaves; i++) { + value += amplitude * perlin_noise_3d(position * frequency); + frequency *= fbm_params.lacunarity; + amplitude *= fbm_params.persistence; + } + + // Normalize to [0, 1] range + return value * 0.5 + 0.5; +} + +// 3D Perlin noise implementation +fn perlin_noise_3d(p: vec3) -> f32 { + let pi = floor(p); + let pf = fract(p); + + // Fade curve + let u = fade(pf); + + // Hash coordinates of the 8 cube corners + let h000 = hash_3d(pi + vec3(0.0, 0.0, 0.0)); + let h100 = hash_3d(pi + vec3(1.0, 0.0, 0.0)); + let h010 = hash_3d(pi + vec3(0.0, 1.0, 0.0)); + let h110 = hash_3d(pi + vec3(1.0, 1.0, 0.0)); + let h001 = hash_3d(pi + vec3(0.0, 0.0, 1.0)); + let h101 = hash_3d(pi + vec3(1.0, 0.0, 1.0)); + let h011 = hash_3d(pi + vec3(0.0, 1.0, 1.0)); + let h111 = hash_3d(pi + vec3(1.0, 1.0, 1.0)); + + // Gradients + let g000 = gradient_3d(h000, pf - vec3(0.0, 0.0, 0.0)); + let g100 = gradient_3d(h100, pf - vec3(1.0, 0.0, 0.0)); + let g010 = gradient_3d(h010, pf - vec3(0.0, 1.0, 0.0)); + let g110 = gradient_3d(h110, pf - vec3(1.0, 1.0, 0.0)); + let g001 = gradient_3d(h001, pf - vec3(0.0, 0.0, 1.0)); + let g101 = gradient_3d(h101, pf - vec3(1.0, 0.0, 1.0)); + let g011 = gradient_3d(h011, pf - vec3(0.0, 1.0, 1.0)); + let g111 = gradient_3d(h111, pf - vec3(1.0, 1.0, 1.0)); + + // Trilinear interpolation + let x00 = mix(g000, g100, u.x); + let x10 = mix(g010, g110, u.x); + let x01 = mix(g001, g101, u.x); + let x11 = mix(g011, g111, u.x); + + let y0 = mix(x00, x10, u.y); + let y1 = mix(x01, x11, u.y); + + return mix(y0, y1, u.z); +} + +// Fade function for smooth interpolation +fn fade(t: vec3) -> vec3 { + return t * t * t * (t * (t * 6.0 - 15.0) + 10.0); +} + +// Hash function for generating pseudo-random values +fn hash_3d(p: vec3) -> f32 { + let p3 = fract(p * 0.1031); + var p_sum = p3.x + p3.y + p3.z; + p_sum = p3.x * p3.y * p3.z + p_sum; + return fract(sin(p_sum) * 43758.5453123); +} + +// Gradient function for Perlin noise +fn gradient_3d(hash: f32, p: vec3) -> f32 { + // Convert hash to gradient direction + let h = u32(hash * 16.0); + let u = select(p.y, p.x, (h & 8u) == 0u); + + var v: f32; + if ((h & 8u) == 0u) { + if ((h & 4u) == 0u) { + v = p.y; + } else { + v = p.z; + } + } else { + if ((h & 4u) == 0u) { + v = p.x; + } else { + v = p.z; + } + } + + let u_sign = select(-u, u, (h & 1u) == 0u); + let v_sign = select(-v, v, (h & 2u) == 0u); + + return u_sign + v_sign; +} + diff --git a/crates/bevy_pbr/src/atmosphere/functions.wgsl b/crates/bevy_pbr/src/atmosphere/functions.wgsl index bd57a0ee83e0e..ea8e32c9a4839 100644 --- a/crates/bevy_pbr/src/atmosphere/functions.wgsl +++ b/crates/bevy_pbr/src/atmosphere/functions.wgsl @@ -1,6 +1,7 @@ #define_import_path bevy_pbr::atmosphere::functions #import bevy_render::maths::{PI, HALF_PI, PI_2, fast_acos, fast_acos_4, fast_atan2, ray_sphere_intersect} +#import bevy_pbr::utils::interleaved_gradient_noise #import bevy_pbr::atmosphere::{ types::Atmosphere, @@ -16,6 +17,15 @@ }, } +#ifdef CLOUDS_ENABLED +#import bevy_pbr::atmosphere::clouds::{ + get_cloud_density, + get_cloud_scattering_coeff, + sample_cloud_contribution, + compute_cloud_shadow, +} +#endif + // NOTE FOR CONVENTIONS: // r: // radius, or distance from planet center @@ -204,10 +214,16 @@ fn sample_scattering_lut(r: f32, neg_LdotV: f32) -> vec3 { } /// 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_scattering: vec3, ray_dir: vec3, world_pos: vec3) -> vec3 { +fn sample_local_inscattering(local_scattering: vec3, ray_dir: vec3, world_pos: vec3, pixel_coords: vec2) -> vec3 { let local_r = length(world_pos); let local_up = normalize(world_pos); var inscattering = vec3(0.0); + + #ifdef CLOUDS_ENABLED + // Sample cloud density at this point + let cloud_density = get_cloud_density(local_r, world_pos); + #endif + for (var light_i: u32 = 0u; light_i < lights.n_directional_lights; light_i++) { let light = &lights.directional_lights[light_i]; @@ -218,8 +234,33 @@ fn sample_local_inscattering(local_scattering: vec3, ray_dir: vec3, wo let neg_LdotV = dot((*light).direction_to_light, ray_dir); 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 scattering_coeff = sample_scattering_lut(local_r, neg_LdotV); + var shadow_factor = transmittance_to_light * f32(!ray_intersects_ground(local_r, mu_light)); + var scattering_coeff = sample_scattering_lut(local_r, neg_LdotV); + + #ifdef CLOUDS_ENABLED + // NUBIS: Add volumetric cloud scattering with proper physical integration + // Clouds contribute to inscattering via Henyey-Greenstein phase function + // Compute volumetric shadow from clouds (light transmittance through clouds) + let cloud_shadow = compute_cloud_shadow(world_pos, (*light).direction_to_light, 8u, pixel_coords); + shadow_factor *= cloud_shadow; + + // Cloud scattering coefficient: σ_s_cloud = density * scattering_coeff + // Using physically correct coefficients (units: m^-1 per unit density) + // Density is normalized [0, 1], coefficients represent actual physical values + // Water droplet clouds have scattering ~0.0008-0.001 m^-1 per unit density + let cloud_scattering_coeff = cloud_density * get_cloud_scattering_coeff(); + + // Henyey-Greenstein phase function for anisotropic cloud scattering + // g ≈ 0.7-0.9 for clouds (strong forward scattering) + let g = 0.85; // Typical asymmetry parameter for water droplets in clouds + let gg = g * g; + let mu = neg_LdotV; // cos(θ) where θ is angle between light and view + // Henyey-Greenstein: p(θ) = (1/4π) * (1 - g²) / (1 + g² - 2g*cos(θ))^(3/2) + let cloud_phase = FRAC_4_PI * (1.0 - gg) / pow(1.0 + gg - 2.0 * g * mu, 1.5); + + // Add cloud scattering contribution: σ_s_cloud * p(θ) + scattering_coeff += cloud_scattering_coeff * cloud_phase; + #endif // Transmittance from scattering event to light source let scattering_factor = shadow_factor * scattering_coeff; @@ -434,6 +475,9 @@ fn raymarch_atmosphere( let up = normalize(pos); let mu = dot(ray_dir, up); + // Convert UV to pixel coordinates for noise jittering + // Assuming viewport size from view uniform (typically available) + let pixel_coords = uv * view.viewport.zw; // Optimization: Reduce sample count at close proximity to the scene let sample_count = mix(1.0, f32(max_samples), saturate(t_max * 0.01)); @@ -456,8 +500,9 @@ fn raymarch_atmosphere( var prev_t = t_start; var optical_depth = vec3(0.0); for (var s = 0.0; s < sample_count; s += 1.0) { - // Linear distribution from atmosphere entry to exit/ground - let t_i = t_start + t_total * (s + MIDPOINT_RATIO) / sample_count; + // Linear distribution from atmosphere entry to exit/ground with jitter + let jitter = interleaved_gradient_noise(pixel_coords, u32(s)); // [0, 1] + let t_i = t_start + t_total * (s + jitter) / sample_count; let dt_i = (t_i - prev_t); prev_t = t_i; @@ -467,16 +512,22 @@ fn raymarch_atmosphere( let absorption = sample_density_lut(local_r, ABSORPTION_DENSITY); let scattering = sample_density_lut(local_r, SCATTERING_DENSITY); - let extinction = absorption + scattering; + var extinction = absorption + scattering; - let sample_optical_depth = extinction * dt_i; + var sample_optical_depth = extinction * dt_i; optical_depth += sample_optical_depth; + + // Note: Cloud extinction is NOT added here - clouds are handled only in inscattering + // This follows NUBIS integration where clouds contribute to light scattering + // but are not part of the atmospheric extinction calculation + let sample_transmittance = exp(-sample_optical_depth); let inscattering = sample_local_inscattering( scattering, ray_dir, - sample_pos + sample_pos, + pixel_coords ); let s_int = (inscattering - inscattering * sample_transmittance) / max(extinction, MIN_EXTINCTION); diff --git a/crates/bevy_pbr/src/atmosphere/mod.rs b/crates/bevy_pbr/src/atmosphere/mod.rs index 908f471f39d1d..10a7086d36a82 100644 --- a/crates/bevy_pbr/src/atmosphere/mod.rs +++ b/crates/bevy_pbr/src/atmosphere/mod.rs @@ -34,6 +34,7 @@ //! [Unreal Engine Implementation]: https://github.com/sebh/UnrealEngineSkyAtmosphere mod environment; +pub mod fbm_noise; mod node; pub mod resources; @@ -71,6 +72,10 @@ use environment::{ prepare_atmosphere_probe_bind_groups, prepare_atmosphere_probe_components, prepare_probe_textures, AtmosphereEnvironmentMap, EnvironmentNode, }; +use fbm_noise::{ + generate_fbm_noise_once, init_fbm_noise_params_buffer, init_fbm_noise_texture, + prepare_fbm_noise_bind_group, FbmNoiseBindGroupLayout, FbmNoisePipeline, NoiseGenerated, +}; use resources::{ prepare_atmosphere_transforms, prepare_atmosphere_uniforms, queue_render_sky_pipelines, AtmosphereTransforms, GpuAtmosphere, RenderSkyBindGroupLayouts, @@ -86,10 +91,23 @@ use self::{ node::{AtmosphereLutsNode, AtmosphereNode, RenderSkyNode}, resources::{ prepare_atmosphere_bind_groups, prepare_atmosphere_textures, AtmosphereBindGroupLayouts, - AtmosphereLutPipelines, AtmosphereSampler, + AtmosphereLutPipelines, AtmosphereSampler, CloudNoiseSampler, }, }; +/// Creates a default CloudLayer entity to ensure there's always at least one uniform available +fn init_default_cloud_layer(mut commands: bevy_ecs::system::Commands) { + commands.spawn(CloudLayer { + cloud_layer_start: 0.0, + cloud_layer_end: 0.0, + cloud_density: 0.0, // Disabled by default + cloud_absorption: 0.0, + cloud_scattering: 0.0, + noise_scale: 1.0, + noise_offset: Vec3::ZERO, + }); +} + #[doc(hidden)] pub struct AtmospherePlugin; @@ -99,6 +117,7 @@ impl Plugin for AtmospherePlugin { load_shader_library!(app, "functions.wgsl"); load_shader_library!(app, "bruneton_functions.wgsl"); load_shader_library!(app, "bindings.wgsl"); + load_shader_library!(app, "clouds.wgsl"); embedded_asset!(app, "transmittance_lut.wgsl"); embedded_asset!(app, "multiscattering_lut.wgsl"); @@ -106,13 +125,16 @@ impl Plugin for AtmospherePlugin { embedded_asset!(app, "aerial_view_lut.wgsl"); embedded_asset!(app, "render_sky.wgsl"); embedded_asset!(app, "environment.wgsl"); + embedded_asset!(app, "fbm_noise_3d.wgsl"); app.add_plugins(( ExtractComponentPlugin::::default(), ExtractComponentPlugin::::default(), ExtractComponentPlugin::::default(), + ExtractComponentPlugin::::default(), UniformComponentPlugin::::default(), UniformComponentPlugin::::default(), + UniformComponentPlugin::::default(), )) .add_systems(Update, prepare_atmosphere_probe_components); @@ -152,15 +174,22 @@ impl Plugin for AtmospherePlugin { .insert_resource(AtmosphereBindGroupLayouts::new()) .init_resource::() .init_resource::() + .init_resource::() .init_resource::() .init_resource::() .init_resource::>() + .init_resource::() + .init_resource::() + .init_resource::() .add_systems( RenderStartup, ( init_atmosphere_probe_layout, init_atmosphere_probe_pipeline, init_atmosphere_buffer, + init_fbm_noise_texture, + init_fbm_noise_params_buffer, + init_default_cloud_layer, ) .chain(), ) @@ -179,6 +208,10 @@ impl Plugin for AtmospherePlugin { prepare_atmosphere_probe_bind_groups.in_set(RenderSystems::PrepareBindGroups), prepare_atmosphere_transforms.in_set(RenderSystems::PrepareResources), prepare_atmosphere_bind_groups.in_set(RenderSystems::PrepareBindGroups), + prepare_fbm_noise_bind_group.in_set(RenderSystems::PrepareBindGroups), + generate_fbm_noise_once + .in_set(RenderSystems::Render) + .after(RenderSystems::PrepareBindGroups), write_atmosphere_buffer.in_set(RenderSystems::PrepareResources), ), ) @@ -463,3 +496,56 @@ pub enum AtmosphereMode { /// accurate long-distance lighting. Raymarched = 1, } + +/// Component that adds a volumetric cloud layer to the atmosphere. +/// Add this component to a camera with [`Atmosphere`] to enable clouds. +#[derive(Clone, Component, Reflect, ShaderType)] +#[reflect(Clone, Default)] +pub struct CloudLayer { + /// Altitude at which the cloud layer starts (from planet center) + /// units: m + pub cloud_layer_start: f32, + + /// Altitude at which the cloud layer ends (from planet center) + /// units: m + pub cloud_layer_end: f32, + + /// Density multiplier for the clouds + pub cloud_density: f32, + + /// Absorption coefficient for clouds + pub cloud_absorption: f32, + + /// Scattering coefficient for clouds + pub cloud_scattering: f32, + + /// Scale of the noise texture in world space + pub noise_scale: f32, + + /// Offset for animating the noise texture + pub noise_offset: Vec3, +} + +impl Default for CloudLayer { + fn default() -> Self { + Self { + cloud_layer_start: 6_361_000.0, // 1km above Earth's surface + cloud_layer_end: 6_365_000.0, // 5km above Earth's surface + cloud_density: 0.3, + cloud_absorption: 0.5, + cloud_scattering: 1.0, + noise_scale: 10000.0, + noise_offset: Vec3::ZERO, + } + } +} + +impl ExtractComponent for CloudLayer { + type QueryData = Read; + type QueryFilter = (With, With); + type Out = CloudLayer; + + fn extract_component(item: QueryItem<'_, '_, Self::QueryData>) -> Option { + Some(item.clone()) + } +} diff --git a/crates/bevy_pbr/src/atmosphere/node.rs b/crates/bevy_pbr/src/atmosphere/node.rs index c49debdba30f4..0d59d39ab68d2 100644 --- a/crates/bevy_pbr/src/atmosphere/node.rs +++ b/crates/bevy_pbr/src/atmosphere/node.rs @@ -16,7 +16,7 @@ use super::{ AtmosphereBindGroups, AtmosphereLutPipelines, AtmosphereTransformsOffset, RenderSkyPipelineId, }, - GpuAtmosphereSettings, + CloudLayer, GpuAtmosphereSettings, }; #[derive(PartialEq, Eq, Debug, Copy, Clone, Hash, RenderLabel)] @@ -173,6 +173,7 @@ impl ViewNode for RenderSkyNode { Read, Read, Read, + Read>, ); fn run<'w>( @@ -188,6 +189,7 @@ impl ViewNode for RenderSkyNode { view_uniforms_offset, lights_uniforms_offset, render_sky_pipeline_id, + cloud_layer_uniforms_offset, ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { @@ -222,6 +224,7 @@ impl ViewNode for RenderSkyNode { atmosphere_transforms_offset.index(), view_uniforms_offset.offset, lights_uniforms_offset.offset, + cloud_layer_uniforms_offset.index(), ], ); render_sky_pass.draw(0..3, 0..1); diff --git a/crates/bevy_pbr/src/atmosphere/render_sky.wgsl b/crates/bevy_pbr/src/atmosphere/render_sky.wgsl index 0e0d5485c963b..fc9302b7410b3 100644 --- a/crates/bevy_pbr/src/atmosphere/render_sky.wgsl +++ b/crates/bevy_pbr/src/atmosphere/render_sky.wgsl @@ -39,7 +39,7 @@ fn main(in: FullscreenVertexOutput) -> RenderSkyOutput { let r = length(world_pos); let up = normalize(world_pos); let mu = dot(ray_dir_ws, up); - let max_samples = settings.sky_max_samples; + let max_samples = 16u; let should_raymarch = settings.rendering_method == 1u; var transmittance: vec3; diff --git a/crates/bevy_pbr/src/atmosphere/resources.rs b/crates/bevy_pbr/src/atmosphere/resources.rs index 1d2657d09837b..c73a151aaa70f 100644 --- a/crates/bevy_pbr/src/atmosphere/resources.rs +++ b/crates/bevy_pbr/src/atmosphere/resources.rs @@ -1,6 +1,6 @@ use crate::{ - ExtractedAtmosphere, GpuLights, GpuScatteringMedium, LightMeta, ScatteringMedium, - ScatteringMediumSampler, + fbm_noise::FbmNoiseTexture, CloudLayer, ExtractedAtmosphere, GpuLights, GpuScatteringMedium, + LightMeta, ScatteringMedium, ScatteringMediumSampler, }; use bevy_asset::{load_embedded_asset, AssetId, Handle}; use bevy_camera::{Camera, Camera3d}; @@ -189,6 +189,13 @@ impl FromWorld for RenderSkyBindGroupLayouts { (12, sampler(SamplerBindingType::Filtering)), // view depth texture (13, texture_2d(TextureSampleType::Depth)), + (14, uniform_buffer::(true)), // cloud layer parameters + ( + // 3D noise texture for clouds + 15, + texture_3d(TextureSampleType::Float { filterable: true }), + ), + (16, sampler(SamplerBindingType::Filtering)), // noise sampler ), ), ); @@ -215,6 +222,13 @@ impl FromWorld for RenderSkyBindGroupLayouts { (12, sampler(SamplerBindingType::Filtering)), // view depth texture (13, texture_2d_multisampled(TextureSampleType::Depth)), + (14, uniform_buffer::(true)), // cloud layer parameters + ( + // 3D noise texture for clouds + 15, + texture_3d(TextureSampleType::Float { filterable: true }), + ), + (16, sampler(SamplerBindingType::Filtering)), // noise sampler ), ), ); @@ -246,6 +260,27 @@ impl FromWorld for AtmosphereSampler { } } +#[derive(Resource, Deref)] +pub struct CloudNoiseSampler(Sampler); + +impl FromWorld for CloudNoiseSampler { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + + let sampler = render_device.create_sampler(&SamplerDescriptor { + address_mode_u: AddressMode::Repeat, + address_mode_v: AddressMode::Repeat, + address_mode_w: AddressMode::Repeat, + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + mipmap_filter: FilterMode::Nearest, + ..Default::default() + }); + + Self(sampler) + } +} + #[derive(Resource)] pub(crate) struct AtmosphereLutPipelines { pub transmittance_lut: CachedComputePipelineId, @@ -278,6 +313,8 @@ impl FromWorld for AtmosphereLutPipelines { label: Some("sky_view_lut_pipeline".into()), layout: vec![layouts.sky_view_lut.clone()], shader: load_embedded_asset!(world, "sky_view_lut.wgsl"), + // Note: Clouds disabled for sky_view_lut - too complex for precomputation + // Clouds are rendered in real-time raymarching instead ..default() }); @@ -319,6 +356,9 @@ impl SpecializedRenderPipeline for RenderSkyBindGroupLayouts { shader_defs.push("DUAL_SOURCE_BLENDING".into()); } + // Enable cloud rendering + shader_defs.push("CLOUDS_ENABLED".into()); + let dst_factor = if key.dual_source_blending { BlendFactor::Src1 } else { @@ -602,7 +642,7 @@ pub(super) fn prepare_atmosphere_bind_groups( render_device: Res, layouts: Res, render_sky_layouts: Res, - atmosphere_sampler: Res, + (atmosphere_sampler, cloud_noise_sampler): (Res, Res), view_uniforms: Res, lights_uniforms: Res, atmosphere_transforms: Res, @@ -611,6 +651,8 @@ pub(super) fn prepare_atmosphere_bind_groups( gpu_media: Res>, medium_sampler: Res, pipeline_cache: Res, + cloud_layer_uniforms: Res>, + fbm_noise_texture: Res, mut commands: Commands, ) -> Result<(), BevyError> { if views.iter().len() == 0 { @@ -640,11 +682,12 @@ pub(super) fn prepare_atmosphere_bind_groups( .binding() .ok_or(AtmosphereBindGroupError::LightUniforms)?; + let cloud_layer_binding = cloud_layer_uniforms.binding().unwrap(); + for (entity, atmosphere, textures, view_depth_texture, msaa) in &views { let gpu_medium = gpu_media .get(atmosphere.medium) .ok_or(ScatteringMediumMissingError(atmosphere.medium))?; - let transmittance_lut = render_device.create_bind_group( "transmittance_lut_bind_group", &pipeline_cache.get_bind_group_layout(&layouts.transmittance_lut), @@ -751,6 +794,9 @@ pub(super) fn prepare_atmosphere_bind_groups( (12, &**atmosphere_sampler), // view depth texture (13, view_depth_texture.view()), + (14, cloud_layer_binding.clone()), + (15, &fbm_noise_texture.texture.default_view), + (16, &**cloud_noise_sampler), )), ); diff --git a/examples/3d/atmosphere.rs b/examples/3d/atmosphere.rs index bd27bf888d509..56d02d82fe2d7 100644 --- a/examples/3d/atmosphere.rs +++ b/examples/3d/atmosphere.rs @@ -18,8 +18,8 @@ use bevy::{ VolumetricFog, VolumetricLight, }, pbr::{ - AtmosphereMode, AtmosphereSettings, DefaultOpaqueRendererMethod, EarthlikeAtmosphere, - ExtendedMaterial, MaterialExtension, ScreenSpaceReflections, + AtmosphereMode, AtmosphereSettings, CloudLayer, DefaultOpaqueRendererMethod, + EarthlikeAtmosphere, ExtendedMaterial, MaterialExtension, ScreenSpaceReflections, }, post_process::bloom::Bloom, prelude::*, @@ -56,6 +56,8 @@ fn print_controls() { println!("Atmosphere Example Controls:"); println!(" 1 - Switch to lookup texture rendering method"); println!(" 2 - Switch to raymarched rendering method"); + println!(" C - Toggle cloud layer"); + println!(" Left/Right - Decrease/Increase cloud density"); println!(" Enter - Pause/Resume sun motion"); println!(" Up/Down - Increase/Decrease exposure"); } @@ -63,6 +65,9 @@ fn print_controls() { fn atmosphere_controls( keyboard_input: Res>, mut atmosphere_settings: Query<&mut AtmosphereSettings>, + mut cloud_layers: Query<&mut CloudLayer>, + mut commands: Commands, + cameras: Query>, mut game_state: ResMut, mut camera_exposure: Query<&mut Exposure, With>, time: Res