|
| 1 | +// Cloud rendering functions using 3D FBM noise |
| 2 | +#define_import_path bevy_pbr::atmosphere::clouds |
| 3 | + |
| 4 | +#import bevy_render::maths::ray_sphere_intersect |
| 5 | +#import bevy_pbr::utils::interleaved_gradient_noise |
| 6 | +#import bevy_pbr::atmosphere::{ |
| 7 | + types::{Atmosphere, AtmosphereSettings}, |
| 8 | + bindings::{settings, atmosphere}, |
| 9 | + functions::{ |
| 10 | + get_local_r, |
| 11 | + }, |
| 12 | +} |
| 13 | + |
| 14 | +struct CloudLayer { |
| 15 | + cloud_layer_start: f32, |
| 16 | + cloud_layer_end: f32, |
| 17 | + cloud_density: f32, |
| 18 | + cloud_absorption: f32, |
| 19 | + cloud_scattering: f32, |
| 20 | + noise_scale: f32, |
| 21 | + noise_offset: vec3<f32>, |
| 22 | +} |
| 23 | + |
| 24 | +@group(0) @binding(14) var<uniform> cloud_layer: CloudLayer; |
| 25 | +@group(0) @binding(15) var noise_texture_3d: texture_3d<f32>; |
| 26 | +@group(0) @binding(16) var noise_sampler_3d: sampler; |
| 27 | + |
| 28 | +/// Sample the 3D noise texture at a world position |
| 29 | +fn sample_cloud_noise(world_pos: vec3<f32>) -> f32 { |
| 30 | + // Convert world position to noise texture coordinates |
| 31 | + let noise_coords = (world_pos + cloud_layer.noise_offset) / cloud_layer.noise_scale; |
| 32 | + |
| 33 | + // Sample the 3D noise texture with wrapping |
| 34 | + return textureSampleLevel(noise_texture_3d, noise_sampler_3d, noise_coords, 0.0).r; |
| 35 | +} |
| 36 | + |
| 37 | +/// Get cloud density at a given position (in local atmosphere space) |
| 38 | +fn get_cloud_density(r: f32, world_pos: vec3<f32>) -> f32 { |
| 39 | + // Check if we're within the cloud layer |
| 40 | + if (r < cloud_layer.cloud_layer_start || r > cloud_layer.cloud_layer_end) { |
| 41 | + return 0.0; |
| 42 | + } |
| 43 | + |
| 44 | + // Calculate height factor within cloud layer (0 at bottom, 1 at top) |
| 45 | + let layer_thickness = cloud_layer.cloud_layer_end - cloud_layer.cloud_layer_start; |
| 46 | + let height_in_layer = r - cloud_layer.cloud_layer_start; |
| 47 | + let height_factor = height_in_layer / layer_thickness; |
| 48 | + |
| 49 | + // Sample noise |
| 50 | + var noise_value = sample_cloud_noise(world_pos); |
| 51 | + noise_value = clamp(pow(noise_value, 3.0), 0.0, 1.0); |
| 52 | + |
| 53 | + // Height-based density falloff (clouds denser in middle of layer) |
| 54 | + let height_gradient = 1.0 - abs(height_factor * 2.0 - 1.0); |
| 55 | + let height_multiplier = smoothstep(0.0, 0.3, height_gradient) * smoothstep(1.0, 0.6, height_gradient); |
| 56 | + |
| 57 | + // Combine noise with height gradient |
| 58 | + let density = noise_value * height_multiplier * 0.01; |
| 59 | + |
| 60 | + return max(0.0, density); |
| 61 | +} |
| 62 | + |
| 63 | +struct CloudSample { |
| 64 | + density: f32, |
| 65 | + scattering: f32, |
| 66 | + absorption: f32, |
| 67 | +} |
| 68 | + |
| 69 | +/// Ray march through the cloud layer |
| 70 | +fn raymarch_clouds( |
| 71 | + ray_origin: vec3<f32>, |
| 72 | + ray_dir: vec3<f32>, |
| 73 | + max_distance: f32, |
| 74 | + steps: u32, |
| 75 | + pixel_coords: vec2<f32>, |
| 76 | +) -> vec4<f32> { |
| 77 | + // Early exit if clouds are disabled (density is 0) |
| 78 | + if (cloud_layer.cloud_density <= 0.0) { |
| 79 | + return vec4(0.0); |
| 80 | + } |
| 81 | + |
| 82 | + let r = length(ray_origin); |
| 83 | + let mu = dot(ray_dir, normalize(ray_origin)); |
| 84 | + |
| 85 | + // Find intersection with cloud layer spheres |
| 86 | + // ray_sphere_intersect returns vec2(near_t, far_t) |
| 87 | + let cloud_bottom_intersect = ray_sphere_intersect(r, mu, cloud_layer.cloud_layer_start); |
| 88 | + let cloud_top_intersect = ray_sphere_intersect(r, mu, cloud_layer.cloud_layer_end); |
| 89 | + |
| 90 | + // Determine ray march bounds through the cloud layer |
| 91 | + var march_start = 0.0; |
| 92 | + var march_end = max_distance; |
| 93 | + |
| 94 | + if (r < cloud_layer.cloud_layer_start) { |
| 95 | + // Below cloud layer - march from cloud bottom to cloud top |
| 96 | + if (cloud_bottom_intersect.y < 0.0) { |
| 97 | + return vec4(0.0); // Ray doesn't hit cloud layer |
| 98 | + } |
| 99 | + march_start = max(0.0, cloud_bottom_intersect.y); |
| 100 | + march_end = min(max_distance, cloud_top_intersect.y); |
| 101 | + } else if (r < cloud_layer.cloud_layer_end) { |
| 102 | + // Inside cloud layer |
| 103 | + march_start = 0.0; |
| 104 | + march_end = min(max_distance, select(cloud_top_intersect.y, cloud_bottom_intersect.x, mu < 0.0)); |
| 105 | + } else { |
| 106 | + // Above cloud layer - march from cloud top to cloud bottom |
| 107 | + if (cloud_top_intersect.x < 0.0) { |
| 108 | + return vec4(0.0); // Ray doesn't hit cloud layer |
| 109 | + } |
| 110 | + march_start = max(0.0, cloud_top_intersect.x); |
| 111 | + march_end = min(max_distance, cloud_bottom_intersect.x); |
| 112 | + } |
| 113 | + |
| 114 | + if (march_start >= march_end) { |
| 115 | + return vec4(0.0); |
| 116 | + } |
| 117 | + |
| 118 | + let march_distance = march_end - march_start; |
| 119 | + let step_size = march_distance / f32(steps); |
| 120 | + |
| 121 | + var cloud_color = vec3(0.0); |
| 122 | + var transmittance = 1.0; |
| 123 | + |
| 124 | + // Generate noise offset for temporal jittering (reduces banding) |
| 125 | + let jitter = interleaved_gradient_noise(pixel_coords, 0u); |
| 126 | + |
| 127 | + // Ray march through cloud layer |
| 128 | + for (var i = 0u; i < steps; i++) { |
| 129 | + if (transmittance < 0.01) { |
| 130 | + break; |
| 131 | + } |
| 132 | + |
| 133 | + // Add jitter to sample position to reduce banding artifacts |
| 134 | + let t = march_start + (f32(i) + jitter) * step_size; |
| 135 | + let sample_pos = ray_origin + ray_dir * t; |
| 136 | + let r = length(sample_pos); |
| 137 | + |
| 138 | + let density = get_cloud_density(r, sample_pos); |
| 139 | + |
| 140 | + if (density > 0.001) { |
| 141 | + let extinction = density * (cloud_layer.cloud_scattering + cloud_layer.cloud_absorption); |
| 142 | + let scattering = density * cloud_layer.cloud_scattering; |
| 143 | + |
| 144 | + // Beer's law for transmittance |
| 145 | + let sample_transmittance = exp(-extinction * step_size); |
| 146 | + |
| 147 | + // Simple lighting (could be improved with light ray marching) |
| 148 | + let light_energy = 1.0; // Simplified - should sample actual lighting |
| 149 | + |
| 150 | + // In-scattering contribution |
| 151 | + // Use safe division to avoid divide-by-zero |
| 152 | + if (extinction > 0.0001) { |
| 153 | + cloud_color += light_energy * scattering * transmittance * (1.0 - sample_transmittance) / extinction; |
| 154 | + } |
| 155 | + |
| 156 | + // Update transmittance |
| 157 | + transmittance *= sample_transmittance; |
| 158 | + } |
| 159 | + } |
| 160 | + |
| 161 | + return vec4(cloud_color, 1.0 - transmittance); |
| 162 | +} |
| 163 | + |
| 164 | +/// Raymarch through clouds towards the sun to compute volumetric shadow |
| 165 | +/// Returns the light transmittance factor [0,1] where 0 = fully shadowed, 1 = no shadow |
| 166 | +fn compute_cloud_shadow( |
| 167 | + world_pos: vec3<f32>, |
| 168 | + sun_dir: vec3<f32>, |
| 169 | + steps: u32, |
| 170 | + pixel_coords: vec2<f32>, |
| 171 | +) -> f32 { |
| 172 | + // Early exit if clouds are disabled |
| 173 | + if (cloud_layer.cloud_density <= 0.0) { |
| 174 | + return 1.0; |
| 175 | + } |
| 176 | + |
| 177 | + let r = length(world_pos); |
| 178 | + let mu = dot(sun_dir, normalize(world_pos)); |
| 179 | + |
| 180 | + // Find intersection with cloud layer in sun direction |
| 181 | + let cloud_bottom_intersect = ray_sphere_intersect(r, mu, cloud_layer.cloud_layer_start); |
| 182 | + let cloud_top_intersect = ray_sphere_intersect(r, mu, cloud_layer.cloud_layer_end); |
| 183 | + |
| 184 | + // Determine march bounds through cloud layer towards sun |
| 185 | + var march_start = 0.0; |
| 186 | + var march_end = 0.0; |
| 187 | + |
| 188 | + if (r < cloud_layer.cloud_layer_start) { |
| 189 | + // Below clouds - march from cloud bottom to top |
| 190 | + if (cloud_bottom_intersect.y < 0.0) { |
| 191 | + return 1.0; // No intersection |
| 192 | + } |
| 193 | + march_start = cloud_bottom_intersect.y; |
| 194 | + march_end = cloud_top_intersect.y; |
| 195 | + } else if (r < cloud_layer.cloud_layer_end) { |
| 196 | + // Inside cloud layer - march to exit point |
| 197 | + march_start = 0.0; |
| 198 | + march_end = select(cloud_top_intersect.y, cloud_bottom_intersect.x, mu < 0.0); |
| 199 | + } else { |
| 200 | + // Above clouds - march from cloud top to bottom |
| 201 | + if (cloud_top_intersect.x < 0.0) { |
| 202 | + return 1.0; // No intersection |
| 203 | + } |
| 204 | + march_start = cloud_top_intersect.x; |
| 205 | + march_end = cloud_bottom_intersect.x; |
| 206 | + } |
| 207 | + |
| 208 | + if (march_start >= march_end || march_end <= 0.0) { |
| 209 | + return 1.0; |
| 210 | + } |
| 211 | + |
| 212 | + let march_distance = march_end - march_start; |
| 213 | + let step_size = march_distance / f32(steps); |
| 214 | + |
| 215 | + var optical_depth = 0.0; |
| 216 | + |
| 217 | + // Generate noise offset for jittering (use different frame offset for shadow rays) |
| 218 | + let jitter = interleaved_gradient_noise(pixel_coords, 1u); |
| 219 | + |
| 220 | + // Raymarch through clouds towards sun |
| 221 | + for (var i = 0u; i < steps; i++) { |
| 222 | + // Add jitter to shadow ray samples |
| 223 | + let t = march_start + (f32(i) + jitter) * step_size; |
| 224 | + let sample_pos = world_pos + sun_dir * t; |
| 225 | + let sample_r = length(sample_pos); |
| 226 | + |
| 227 | + let density = get_cloud_density(sample_r, sample_pos); |
| 228 | + |
| 229 | + if (density > 0.001) { |
| 230 | + let extinction = density * (cloud_layer.cloud_scattering + cloud_layer.cloud_absorption); |
| 231 | + optical_depth += extinction * step_size; |
| 232 | + |
| 233 | + // Early exit if fully shadowed |
| 234 | + if (optical_depth > 5.0) { |
| 235 | + return 0.0; |
| 236 | + } |
| 237 | + } |
| 238 | + } |
| 239 | + |
| 240 | + // Beer-Lambert law for shadow transmission |
| 241 | + return exp(-optical_depth); |
| 242 | +} |
| 243 | + |
| 244 | +/// Simplified cloud contribution for a single sample point |
| 245 | +/// Returns (luminance_added, transmittance_multiplier) |
| 246 | +fn sample_cloud_contribution( |
| 247 | + world_pos: vec3<f32>, |
| 248 | + step_size: f32, |
| 249 | +) -> vec2<f32> { |
| 250 | + let r = length(world_pos); |
| 251 | + let density = get_cloud_density(r, world_pos); |
| 252 | + |
| 253 | + if (density < 0.001) { |
| 254 | + return vec2(0.0, 1.0); |
| 255 | + } |
| 256 | + |
| 257 | + let extinction = density * (cloud_layer.cloud_scattering + cloud_layer.cloud_absorption); |
| 258 | + let scattering = density * cloud_layer.cloud_scattering; |
| 259 | + |
| 260 | + // Beer's law |
| 261 | + let transmittance = exp(-extinction * step_size); |
| 262 | + |
| 263 | + // Simple uniform scattering (could be enhanced with actual sun direction) |
| 264 | + let in_scatter = scattering * (1.0 - transmittance); |
| 265 | + |
| 266 | + return vec2(in_scatter, transmittance); |
| 267 | +} |
0 commit comments