Search across packages, versions, files, declarations, and comments. Use multiple terms to narrow results (AND), and add @vertex, @fragment, or @compute to filter by shader stage.
1//! A postprocessing shader that implements volumetric fog via raymarching and
2//! sampling directional light shadow maps.
3//!
4//! The overall approach is a combination of the volumetric rendering in [1] and
5//! the shadow map raymarching in [2]. First, we raytrace the AABB of the fog
6//! volume in order to determine how long our ray is. Then we do a raymarch, with
7//! physically-based calculations at each step to determine how much light was
8//! absorbed, scattered out, and scattered in. To determine in-scattering, we
9//! sample the shadow map for the light to determine whether the point was in
10//! shadow or not.
11//!
12//! [1]: https://www.scratchapixel.com/lessons/3d-basic-rendering/volume-rendering-for-developers/intro-volume-rendering.html
13//!
14//! [2]: http://www.alexandre-pestana.com/volumetric-lights/
16import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput;
17import package::atmosphere::{
18 functions::{calculate_visible_sun_ratio, clamp_to_surface},
19 bruneton_functions::transmittance_lut_r_mu_to_uv,
20};
21import package::render::mesh_functions::{get_world_from_local, mesh_position_local_to_clip};
22import package::render::mesh_view_bindings::{
23 globals, lights, view, clustered_lights,
24 atmosphere, atmosphere_transmittance_texture, atmosphere_transmittance_sampler
25};
26import package::render::mesh_view_types::{
27 DIRECTIONAL_LIGHT_FLAGS_VOLUMETRIC_BIT,
28 POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT,
29 POINT_LIGHT_FLAGS_VOLUMETRIC_BIT,
30 POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE,
31};
32import package::render::shadow_sampling::{
33 sample_shadow_map_hardware,
34 sample_shadow_cubemap,
35 sample_shadow_map,
36 SPOT_SHADOW_TEXEL_SIZE
37};
38import package::render::shadows::{get_cascade_index, world_to_directional_light_local};
39import package::render::utils::interleaved_gradient_noise;
40import package::render::view_transformations::{
41 depth_ndc_to_view_z,
42 frag_coord_to_ndc,
43 position_ndc_to_view,
44 position_ndc_to_world,
45 position_view_to_world
46};
47import bevy_render::maths::orthonormalize;
48import package::render::clustered_forward as clustering;
49import package::render::pbr_lighting::getDistanceAttenuation;
51/// The GPU version of [`VolumetricFog`]. See the comments in
52/// `volumetric_fog/mod.rs` for descriptions of the fields here.
53struct VolumetricFog {
54 clip_from_local: mat4x4<f32>,
55 uvw_from_world: mat4x4<f32>,
56 far_planes: array<vec4<f32>, 6>,
57 fog_color: vec3<f32>,
58 light_tint: vec3<f32>,
59 ambient_color: vec3<f32>,
60 ambient_intensity: f32,
61 step_count: u32,
62 bounding_radius: f32,
63 absorption: f32,
64 scattering: f32,
65 density_factor: f32,
66 density_texture_offset: vec3<f32>,
67 scattering_asymmetry: f32,
68 light_intensity: f32,
69 jitter_strength: f32,
72@group(1) @binding(0) var<uniform> volumetric_fog: VolumetricFog;
74@if(MULTISAMPLED)
75@group(1) @binding(1) var depth_texture: texture_depth_multisampled_2d;
76@else
77@group(1) @binding(1) var depth_texture: texture_depth_2d;
79@if(DENSITY_TEXTURE) {
80@group(1) @binding(2) var density_texture: texture_3d<f32>;
81@group(1) @binding(3) var density_sampler: sampler;
84/// 1 / (4π)
85const FRAC_4_PI: f32 = 0.07957747154594767;
87struct Vertex {
88 @builtin(instance_index) instance_index: u32,
89 @location(0) position: vec3<f32>,
92@vertex
93fn vertex(vertex: Vertex) -> @builtin(position) vec4<f32> {
94 return volumetric_fog.clip_from_local * vec4<f32>(vertex.position, 1.0);
97/// The common Henyey-Greenstein asymmetric phase function [1] [2].
98///
99/// This determines how much light goes toward the viewer as opposed to away from
100/// the viewer. From a visual point of view, it controls how the light shafts
101/// appear and disappear as the camera looks at the light source.
102///
103/// [1]: https://www.scratchapixel.com/lessons/3d-basic-rendering/volume-rendering-for-developers/ray-marching-get-it-right.html
104///
105/// [2]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions#TheHenyeyndashGreensteinPhaseFunction
106fn henyey_greenstein(neg_LdotV: f32) -> f32 {
107 let g = volumetric_fog.scattering_asymmetry;
108 let denom = 1.0 + g * g - 2.0 * g * neg_LdotV;
109 return FRAC_4_PI * (1.0 - g * g) / (denom * sqrt(denom));
112@fragment
113fn fragment(@builtin(position) position: vec4<f32>) -> @location(0) vec4<f32> {
114 // Unpack the `volumetric_fog` settings.
115 let uvw_from_world = volumetric_fog.uvw_from_world;
116 let fog_color = volumetric_fog.fog_color;
117 let ambient_color = volumetric_fog.ambient_color;
118 let ambient_intensity = volumetric_fog.ambient_intensity;
119 let step_count = volumetric_fog.step_count;
120 let bounding_radius = volumetric_fog.bounding_radius;
121 let absorption = volumetric_fog.absorption;
122 let scattering = volumetric_fog.scattering;
123 let density_factor = volumetric_fog.density_factor;
124 let density_texture_offset = volumetric_fog.density_texture_offset;
125 let light_tint = volumetric_fog.light_tint;
126 let light_intensity = volumetric_fog.light_intensity;
127 let jitter_strength = volumetric_fog.jitter_strength;
129 // Unpack the view.
130 let exposure = view.exposure;
132 // Sample the depth to put an upper bound on the length of the ray (as we
133 // shouldn't trace through solid objects). If this is multisample, just use
134 // sample 0; this is approximate but good enough.
135 let frag_coord = position;
136 let ndc_end_depth_from_buffer = textureLoad(depth_texture, vec2<i32>(frag_coord.xy), 0);
137 let view_end_depth_from_buffer = -position_ndc_to_view(
138 frag_coord_to_ndc(vec4(position.xy, ndc_end_depth_from_buffer, 1.0))).z;
140 // Calculate the start position of the ray. Since we're only rendering front
141 // faces of the AABB, this is the current fragment's depth.
142 let view_start_pos = position_ndc_to_view(frag_coord_to_ndc(frag_coord));
144 // Calculate the end position of the ray. This requires us to raytrace the
145 // back faces of the AABB to find the one that our ray intersects.
146 var end_depth_view = 0.0;
147 for (var plane_index = 0; plane_index < 6; plane_index += 1) {
148 let plane = volumetric_fog.far_planes[plane_index];
149 let other_plane_a = volumetric_fog.far_planes[(plane_index + 1) % 6];
150 let other_plane_b = volumetric_fog.far_planes[(plane_index + 2) % 6];
151 let other_plane_c = volumetric_fog.far_planes[(plane_index + 3) % 6];
152 let other_plane_d = volumetric_fog.far_planes[(plane_index + 4) % 6];
153 let other_plane_e = volumetric_fog.far_planes[(plane_index + 5) % 6];
155 // Calculate the intersection of the ray and the plane. The ray must
156 // intersect in front of us (t > 0).
157 let t = -plane.w / dot(plane.xyz, view_start_pos.xyz);
158 if (t < 0.0) {
159 continue;
160 }
161 let hit_pos = view_start_pos.xyz * t;
163 // The intersection point must be in front of the other backfaces.
164 // If those tests pass, we found our backface.
165 if (dot(vec4(hit_pos, 1.0), other_plane_a) >= 0.0 &&
166 dot(vec4(hit_pos, 1.0), other_plane_b) >= 0.0 &&
167 dot(vec4(hit_pos, 1.0), other_plane_c) >= 0.0 &&
168 dot(vec4(hit_pos, 1.0), other_plane_d) >= 0.0 &&
169 dot(vec4(hit_pos, 1.0), other_plane_e) >= 0.0)
170 {
171 end_depth_view = -hit_pos.z;
172 break;
173 }
174 }
176 // Starting at the end depth, which we got above, figure out how long the
177 // ray we want to trace is and the length of each increment.
178 end_depth_view = min(end_depth_view, view_end_depth_from_buffer);
180 // We assume world and view have the same scale here.
181 let start_depth_view = -depth_ndc_to_view_z(frag_coord.z);
183 let ray_length_view = max(0.0, end_depth_view - start_depth_view);
184 // If the end is behind the start of the first opaque pixel, then we know it
185 // is occluded, and we don't need to render it
186 if (ray_length_view == 0.0) {
187 return vec4(0.0, 0.0, 0.0, 0.0);
188 }
189 let inv_step_count = 1.0 / f32(step_count);
190 let step_size_world = ray_length_view * inv_step_count;
192 let directional_light_count = lights.n_directional_lights;
194 // Calculate the ray origin (`Ro`) and the ray direction (`Rd`) in NDC,
195 // view, and world coordinates.
196 let Rd_ndc = vec3(frag_coord_to_ndc(position).xy, 1.0);
197 let Rd_view = normalize(position_ndc_to_view(Rd_ndc));
198 var Ro_world = position_view_to_world(view_start_pos.xyz);
199 let Rd_world = normalize(position_ndc_to_world(Rd_ndc) - view.world_position);
201 // Offset by jitter.
202 let jitter = interleaved_gradient_noise(position.xy, globals.frame_count) * jitter_strength;
203 Ro_world += Rd_world * jitter;
205 // Use Beer's law [1] [2] to calculate the maximum amount of light that each
206 // directional light could contribute, and modulate that value by the light
207 // tint and fog color. (The actual value will in turn be modulated by the
208 // phase according to the Henyey-Greenstein formula.)
209 //
210 // [1]: https://www.scratchapixel.com/lessons/3d-basic-rendering/volume-rendering-for-developers/intro-volume-rendering.html
211 //
212 // [2]: https://en.wikipedia.org/wiki/Beer%E2%80%93Lambert_law
214 // Use Beer's law again to accumulate the ambient light all along the path.
215 var accumulated_color = exp(-ray_length_view * (absorption + scattering)) * ambient_color *
216 ambient_intensity;
218 // This is the amount of the background that shows through. We're actually
219 // going to recompute this over and over again for each directional light,
220 // coming up with the same values each time.
221 var background_alpha = 1.0;
223 // If we have a density texture, transform to its local space.
224 @if(DENSITY_TEXTURE)
225 let Ro_uvw = (uvw_from_world * vec4(Ro_world, 1.0)).xyz;
226 @if(DENSITY_TEXTURE)
227 let Rd_step_uvw = mat3x3(uvw_from_world[0].xyz, uvw_from_world[1].xyz, uvw_from_world[2].xyz) *
228 (Rd_world * step_size_world);
230 for (var light_index = 0u; light_index < directional_light_count; light_index += 1u) {
231 // Volumetric lights are all sorted first, so the first time we come to
232 // a non-volumetric light, we know we've seen them all.
233 let light = &lights.directional_lights[light_index];
234 if (((*light).flags & DIRECTIONAL_LIGHT_FLAGS_VOLUMETRIC_BIT) == 0) {
235 break;
236 }
238 let L = (*light).direction_to_light.xyz;
240 // Offset the depth value by the bias.
241 let depth_offset = (*light).shadow_depth_bias * L;
243 // Compute phase, which determines the fraction of light that's
244 // scattered toward the camera instead of away from it.
245 let neg_LdotV = dot(normalize(L), Rd_world);
246 let phase = henyey_greenstein(neg_LdotV);
248 // Reset `background_alpha` for a new raymarch.
249 background_alpha = 1.0;
251 // Start raymarching.
252 for (var step = 0u; step < step_count; step += 1u) {
253 // As an optimization, break if we've gotten too dark.
254 if (background_alpha < 0.001) {
255 break;
256 }
258 // Calculate where we are in the ray.
259 let P_world = Ro_world + Rd_world * f32(step) * step_size_world;
260 let P_view = view_start_pos + Rd_view * f32(step) * step_size_world;
262 var density = density_factor;
263 // Take the density texture into account, if there is one.
264 //
265 // The uvs should never go outside the (0, 0, 0) to (1, 1, 1) box,
266 // but sometimes due to floating point error they can. Handle this
267 // case.
268 @if(DENSITY_TEXTURE)
269 let P_uvw = Ro_uvw + Rd_step_uvw * f32(step);
270 @if(DENSITY_TEXTURE)
271 if (all(P_uvw >= vec3(0.0)) && all(P_uvw <= vec3(1.0))) {
272 density *= textureSampleLevel(density_texture, density_sampler, P_uvw + density_texture_offset, 0.0).r;
273 } else {
274 density = 0.0;
275 }
277 // Calculate absorption (amount of light absorbed by the fog) and
278 // out-scattering (amount of light the fog scattered away).
279 let sample_attenuation = exp(-step_size_world * density * (absorption + scattering));
281 // Process absorption and out-scattering.
282 background_alpha *= sample_attenuation;
284 // Compute in-scattering (amount of light other fog particles
285 // scattered into this ray). This is where any directional light is
286 // scattered in.
288 // Prepare to sample the shadow map.
289 let cascade_index = get_cascade_index(light_index, P_view.z);
290 let light_local = world_to_directional_light_local(
291 light_index,
292 cascade_index,
293 vec4(P_world + depth_offset, 1.0)
294 );
296 // If we're outside the shadow map entirely, local light attenuation
297 // is zero.
298 var local_light_attenuation = f32(light_local.w != 0.0);
300 // Otherwise, sample the shadow map to determine whether, and by how
301 // much, this sample is in the light.
302 if (local_light_attenuation != 0.0) {
303 let cascade = &(*light).cascades[cascade_index];
304 let array_index = i32((*light).depth_texture_base_index + cascade_index);
305 local_light_attenuation =
306 sample_shadow_map_hardware(light_local.xy, light_local.z, array_index);
307 }
309 if (local_light_attenuation != 0.0) {
310 let light_attenuation = exp(-density * bounding_radius * (absorption + scattering));
311 var light_factors_per_step = fog_color * light_tint * light_attenuation *
312 scattering * density * step_size_world * light_intensity * exposure;
314@if(ATMOSPHERE) {
315 // attenuate by atmospheric scattering
316 let P = P_world + depth_offset;
317 let P_as = (atmosphere.world_to_atmosphere * vec4(P, 1.0)).xyz;
318 let P_clamped = clamp_to_surface(atmosphere, P_as);
319 let r = length(P_clamped);
320 let local_up = normalize(P_clamped);
321 let mu_light = dot(L, local_up);
323 let transmittance = sample_transmittance_lut(r, mu_light);
324 let sun_visibility = calculate_visible_sun_ratio(atmosphere, r, mu_light, (*light).sun_disk_angular_size);
325 light_factors_per_step *= transmittance * sun_visibility;
328 // Modulate the factor we calculated above by the phase, fog color,
329 // light color, light tint.
330 let light_color_per_step = (*light).color.rgb * phase * light_factors_per_step;
332 // Accumulate the light.
333 accumulated_color += light_color_per_step * local_light_attenuation *
334 background_alpha;
335 }
336 }
337 }
339 // Point lights and Spot lights
340 let is_orthographic = view.clip_from_view[3].w == 1.0;
342 // Reset `background_alpha` for a new raymarch.
343 background_alpha = 1.0;
345 // Start raymarching.
346 for (var step = 0u; step < step_count; step += 1u) {
347 // As an optimization, break if we've gotten too dark.
348 if (background_alpha < 0.001) {
349 break;
350 }
352 // Calculate where we are in the ray.
353 let P_world = Ro_world + Rd_world * f32(step) * step_size_world;
354 let P_view = view_start_pos + Rd_view * f32(step) * step_size_world;
356 var density = density_factor;
357 // Take the density texture into account, if there is one.
358 //
359 // The uvs should never go outside the (0, 0, 0) to (1, 1, 1) box,
360 // but sometimes due to floating point error they can. Handle this
361 // case.
362 @if(DENSITY_TEXTURE)
363 let P_uvw = Ro_uvw + Rd_step_uvw * f32(step);
364 @if(DENSITY_TEXTURE)
365 if (all(P_uvw >= vec3(0.0)) && all(P_uvw <= vec3(1.0))) {
366 density *= textureSampleLevel(density_texture, density_sampler, P_uvw + density_texture_offset, 0.0).r;
367 } else {
368 density = 0.0;
369 }
371 var sample_color = vec3(0.0);
373 let cluster_index = clustering::view_fragment_cluster_index(frag_coord.xy, P_view.z, is_orthographic);
374 var clusterable_object_index_ranges = clustering::unpack_clusterable_object_index_ranges(cluster_index);
375 for (var i: u32 = clusterable_object_index_ranges.first_point_light_index_offset;
376 i < clusterable_object_index_ranges.first_rect_light_index_offset;
377 i = i + 1u)
378 {
379 let light_id = clustering::get_clusterable_object_id(i);
380 let light = &clustered_lights.data[light_id];
381 if (((*light).flags & POINT_LIGHT_FLAGS_VOLUMETRIC_BIT) == 0) {
382 continue;
383 }
385 let light_to_frag = (*light).position_radius.xyz - P_world;
386 let V = Rd_world;
387 let L = normalize(light_to_frag);
388 let distance_square = dot(light_to_frag, light_to_frag);
389 let distance_atten = getDistanceAttenuation(distance_square, (*light).color_inverse_square_range.w);
390 var local_light_attenuation = distance_atten;
391 if (i < clusterable_object_index_ranges.first_spot_light_index_offset) {
392 var shadow: f32 = 1.0;
393 if (((*light).flags & POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) {
394 shadow = fetch_point_shadow_without_normal(light_id, vec4(P_world, 1.0), position.xy);
395 }
396 local_light_attenuation *= shadow;
397 } else {
398 // spot light attenuation
399 // reconstruct spot dir from x/z and y-direction flag
400 var spot_dir = vec3<f32>((*light).light_custom_data.x, 0.0, (*light).light_custom_data.y);
401 spot_dir.y = sqrt(max(0.0, 1.0 - spot_dir.x * spot_dir.x - spot_dir.z * spot_dir.z));
402 if ((*light).flags & POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE) != 0u {
403 spot_dir.y = -spot_dir.y;
404 }
406 // calculate attenuation based on filament formula https://google.github.io/filament/Filament.md.html#listing_glslpunctuallight
407 // spot_scale and spot_offset have been precomputed
408 // note we normalize here to get "l" from the filament listing. spot_dir is already normalized
409 let cd = dot(-spot_dir, normalize(light_to_frag));
410 let attenuation = saturate(cd * (*light).light_custom_data.z + (*light).light_custom_data.w);
411 let spot_attenuation = attenuation * attenuation;
413 var shadow: f32 = 1.0;
414 if (((*light).flags & POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) {
415 shadow = fetch_spot_shadow_without_normal(light_id, vec4(P_world, 1.0), position.xy);
416 }
417 local_light_attenuation *= spot_attenuation * shadow;
418 }
420 let light_attenuation = exp(-density * bounding_radius * (absorption + scattering));
421 let light_factors_per_step = fog_color * light_tint * light_attenuation *
422 scattering * density * step_size_world * light_intensity * exposure;
424 // Modulate the factor we calculated above by the phase, fog color,
425 // light color, light tint.
426 let light_color_per_step = (*light).color_inverse_square_range.rgb * light_factors_per_step;
428 // Accumulate the light.
429 sample_color += light_color_per_step * local_light_attenuation;
430 }
432 // Calculate absorption (amount of light absorbed by the fog) and
433 // out-scattering (amount of light the fog scattered away).
434 let sample_attenuation = exp(-step_size_world * density * (absorption + scattering));
436 // Process absorption and out-scattering.
437 background_alpha *= sample_attenuation;
438 accumulated_color += sample_color * background_alpha;
439 }
441 // We're done! Return the color with alpha so it can be blended onto the
442 // render target.
443 return vec4(accumulated_color, 1.0 - background_alpha);
446fn fetch_point_shadow_without_normal(light_id: u32, frag_position: vec4<f32>, frag_coord_xy: vec2<f32>) -> f32 {
447 let light = &clustered_lights.data[light_id];
449 // because the shadow maps align with the axes and the frustum planes are at 45 degrees
450 // we can get the worldspace depth by taking the largest absolute axis
451 let surface_to_light = (*light).position_radius.xyz - frag_position.xyz;
452 let surface_to_light_abs = abs(surface_to_light);
453 let distance_to_light = max(surface_to_light_abs.x, max(surface_to_light_abs.y, surface_to_light_abs.z));
455 // The normal bias here is already scaled by the texel size at 1 world unit from the light.
456 // The texel size increases proportionally with distance from the light so multiplying by
457 // distance to light scales the normal bias to the texel size at the fragment distance.
458 let depth_offset = (*light).shadow_depth_bias * normalize(surface_to_light.xyz);
459 let offset_position = frag_position.xyz + depth_offset;
461 // similar largest-absolute-axis trick as above, but now with the offset fragment position
462 let frag_ls = offset_position.xyz - (*light).position_radius.xyz;
463 let abs_position_ls = abs(frag_ls);
464 let major_axis_magnitude = max(abs_position_ls.x, max(abs_position_ls.y, abs_position_ls.z));
466 // NOTE: These simplifications come from multiplying:
467 // projection * vec4(0, 0, -major_axis_magnitude, 1.0)
468 // and keeping only the terms that have any impact on the depth.
469 // Projection-agnostic approach:
470 let zw = -major_axis_magnitude * (*light).light_custom_data.xy + (*light).light_custom_data.zw;
471 let depth = zw.x / zw.y;
473 // Do the lookup, using HW PCF and comparison. Cubemaps assume a left-handed coordinate space,
474 // so we have to flip the z-axis when sampling.
475 let flip_z = vec3(1.0, 1.0, -1.0);
476 return sample_shadow_cubemap(frag_ls * flip_z, distance_to_light, depth, light_id, frag_coord_xy);
479fn fetch_spot_shadow_without_normal(light_id: u32, frag_position: vec4<f32>, frag_coord_xy: vec2<f32>) -> f32 {
480 let light = &clustered_lights.data[light_id];
482 let surface_to_light = (*light).position_radius.xyz - frag_position.xyz;
484 // construct the light view matrix
485 var spot_dir = vec3<f32>((*light).light_custom_data.x, 0.0, (*light).light_custom_data.y);
486 // reconstruct spot dir from x/z and y-direction flag
487 spot_dir.y = sqrt(max(0.0, 1.0 - spot_dir.x * spot_dir.x - spot_dir.z * spot_dir.z));
488 if (((*light).flags & POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE) != 0u) {
489 spot_dir.y = -spot_dir.y;
490 }
492 // view matrix z_axis is the reverse of transform.forward()
493 let fwd = -spot_dir;
494 let offset_position =
495 -surface_to_light
496 + ((*light).shadow_depth_bias * normalize(surface_to_light));
498 let light_inv_rot = orthonormalize(fwd);
500 // because the matrix is a pure rotation matrix, the inverse is just the transpose, and to calculate
501 // the product of the transpose with a vector we can just post-multiply instead of pre-multiplying.
502 // this allows us to keep the matrix construction code identical between CPU and GPU.
503 let projected_position = offset_position * light_inv_rot;
505 // divide xy by perspective matrix "f" and by -projected.z (projected.z is -projection matrix's w)
506 // to get ndc coordinates
507 let f_div_minus_z = 1.0 / ((*light).spot_light_tan_angle * -projected_position.z);
508 let shadow_xy_ndc = projected_position.xy * f_div_minus_z;
509 // convert to uv coordinates
510 let shadow_uv = shadow_xy_ndc * vec2<f32>(0.5, -0.5) + vec2<f32>(0.5, 0.5);
512 // 0.1 must match POINT_LIGHT_NEAR_Z
513 let depth = 0.1 / -projected_position.z;
515 return sample_shadow_map(
516 shadow_uv,
517 depth,
518 i32(light_id) + lights.spot_light_shadowmap_offset,
519 frag_coord_xy,
520 SPOT_SHADOW_TEXEL_SIZE
521 );
524@if(ATMOSPHERE)
525fn sample_transmittance_lut(r: f32, mu: f32) -> vec3<f32> {
526 let uv = transmittance_lut_r_mu_to_uv(atmosphere, r, mu);
527 return textureSampleLevel(
528 atmosphere_transmittance_texture,
529 atmosphere_transmittance_sampler, uv, 0.0).rgb;
Contents