Skip to content

Commit 3fdd05f

Browse files
committed
Cloud scattering prototype
1 parent be4e9c4 commit 3fdd05f

9 files changed

Lines changed: 904 additions & 15 deletions

File tree

crates/bevy_pbr/src/atmosphere/aerial_view_lut.wgsl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
fn main(@builtin(global_invocation_id) idx: vec3<u32>) {
2222
if any(idx.xy > settings.aerial_view_lut_size.xy) { return; }
2323

24-
let uv = (vec2<f32>(idx.xy) + 0.5) / vec2<f32>(settings.aerial_view_lut_size.xy);
24+
// Use global invocation ID as pixel coordinates for jittering
25+
let pixel_coords = vec2<f32>(idx.xy);
26+
let uv = (pixel_coords + 0.5) / vec2<f32>(settings.aerial_view_lut_size.xy);
2527
let ray_dir = uv_to_ray_direction(uv);
2628
let world_pos = get_view_position();
2729

@@ -50,7 +52,7 @@ fn main(@builtin(global_invocation_id) idx: vec3<u32>) {
5052
let sample_transmittance = exp(-sample_optical_depth);
5153

5254
// evaluate one segment of the integral
53-
var inscattering = sample_local_inscattering(scattering, ray_dir, sample_pos);
55+
var inscattering = sample_local_inscattering(scattering, ray_dir, sample_pos, pixel_coords);
5456

5557
// Analytical integration of the single scattering term in the radiance transfer equation
5658
let s_int = (inscattering - inscattering * sample_transmittance) / max(extinction, MIN_EXTINCTION);
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
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

Comments
 (0)