Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Atmosphere LUT parameterization improvements #17555

Merged
merged 10 commits into from
Feb 3, 2025
15 changes: 11 additions & 4 deletions crates/bevy_pbr/src/atmosphere/aerial_view_lut.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
sample_transmittance_lut, sample_atmosphere, rayleigh, henyey_greenstein,
sample_multiscattering_lut, AtmosphereSample, sample_local_inscattering,
get_local_r, get_local_up, view_radius, uv_to_ndc, max_atmosphere_distance,
uv_to_ray_direction
uv_to_ray_direction, MIDPOINT_RATIO
},
}
}
Expand All @@ -32,7 +32,10 @@ fn main(@builtin(global_invocation_id) idx: vec3<u32>) {

for (var slice_i: u32 = 0; slice_i < settings.aerial_view_lut_size.z; slice_i++) {
for (var step_i: u32 = 0; step_i < settings.aerial_view_lut_samples; step_i++) {
let t_i = t_max * (f32(slice_i) + ((f32(step_i) + 0.5) / f32(settings.aerial_view_lut_samples))) / f32(settings.aerial_view_lut_size.z);
// Offset by -0.5 to align sampling position with slice boundaries,
// since each texel stores the integral over its entire slice
var t_i = t_max * (f32(slice_i) - 0.5 + ((f32(step_i) + MIDPOINT_RATIO) / f32(settings.aerial_view_lut_samples))) / f32(settings.aerial_view_lut_size.z);
t_i = max(t_i, 0.0);
let dt = (t_i - prev_t);
prev_t = t_i;

Expand All @@ -55,8 +58,12 @@ fn main(@builtin(global_invocation_id) idx: vec3<u32>) {
break;
}
}
//We only have one channel to store transmittance, so we store the mean
// We only have one channel to store transmittance, so we store the mean
let mean_transmittance = (throughput.r + throughput.g + throughput.b) / 3.0;
textureStore(aerial_view_lut_out, vec3(vec2<u32>(idx.xy), slice_i), vec4(total_inscattering, mean_transmittance));

// Store the optical depth so that it can be linearly sampled between slices
let optical_depth = -log(max(mean_transmittance, 1e-6)); // Avoid log(0)
let log_inscattering = log(max(total_inscattering, vec3(1e-6)));
textureStore(aerial_view_lut_out, vec3(vec2<u32>(idx.xy), slice_i), vec4(log_inscattering, optical_depth));
}
}
64 changes: 36 additions & 28 deletions crates/bevy_pbr/src/atmosphere/functions.wgsl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#define_import_path bevy_pbr::atmosphere::functions

#import bevy_render::maths::{PI, HALF_PI, PI_2, fast_acos, fast_atan2}
#import bevy_render::maths::{PI, HALF_PI, PI_2, fast_acos, fast_acos_4, fast_atan2}

#import bevy_pbr::atmosphere::{
types::Atmosphere,
Expand Down Expand Up @@ -38,11 +38,17 @@
// CONSTANTS

const FRAC_PI: f32 = 0.3183098862; // 1 / π
const FRAC_2_PI: f32 = 0.15915494309;
const FRAC_2_PI: f32 = 0.15915494309; // 1 / (2π)
const FRAC_3_16_PI: f32 = 0.0596831036594607509; // 3 / (16π)
const FRAC_4_PI: f32 = 0.07957747154594767; // 1 / (4π)
const ROOT_2: f32 = 1.41421356; // √2

// During raymarching, each segment is sampled at a single point. This constant determines
// where in the segment that sample is taken (0.0 = start, 0.5 = middle, 1.0 = end).
// We use 0.3 to sample closer to the start of each segment, which better approximates
// the exponential falloff of atmospheric density.
const MIDPOINT_RATIO: f32 = 0.3;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be exposed as a uniform variable, since it can differ depending on the scale height of the scatterers in the atmosphere.

Copy link
Contributor

@ecoskey ecoskey Feb 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

possibly, if it doesn't add more complexity to the user-facing api. Alternatively we could also just do the integration differently, for example piecewise linearly or quadratically (trapezoid rule vs simpson's rule)


// LUT UV PARAMATERIZATIONS

fn unit_to_sub_uvs(val: vec2<f32>, resolution: vec2<f32>) -> vec2<f32> {
Expand Down Expand Up @@ -71,41 +77,38 @@ fn sky_view_lut_r_mu_azimuth_to_uv(r: f32, mu: f32, azimuth: f32) -> vec2<f32> {

let v_horizon = sqrt(r * r - atmosphere.bottom_radius * atmosphere.bottom_radius);
let cos_beta = v_horizon / r;
let beta = fast_acos(cos_beta);
// Using fast_acos_4 for better precision at small angles
// to avoid artifacts at the horizon
let beta = fast_acos_4(cos_beta);
let horizon_zenith = PI - beta;
let view_zenith = fast_acos(mu);

var v: f32;
if !ray_intersects_ground(r, mu) {
let coord = sqrt(1.0 - view_zenith / horizon_zenith);
v = (1.0 - coord) * 0.5;
} else {
let coord = (view_zenith - horizon_zenith) / beta;
v = sqrt(coord) * 0.5 + 0.5;
}
let view_zenith = fast_acos_4(mu);

// Apply non-linear transformation to compress more texels
// near the horizon where high-frequency details matter most
// l is latitude in [-π/2, π/2] and v is texture coordinate in [0,1]
let l = view_zenith - horizon_zenith;
let abs_l = abs(l);

let v = 0.5 + 0.5 * sign(l) * sqrt(abs_l / HALF_PI);

return unit_to_sub_uvs(vec2(u, v), vec2<f32>(settings.sky_view_lut_size));
}

fn sky_view_lut_uv_to_zenith_azimuth(r: f32, uv: vec2<f32>) -> vec2<f32> {
let adj_uv = sub_uvs_to_unit(uv, vec2<f32>(settings.sky_view_lut_size));
let adj_uv = sub_uvs_to_unit(vec2(uv.x, 1.0 - uv.y), vec2<f32>(settings.sky_view_lut_size));
let azimuth = (adj_uv.x - 0.5) * PI_2;

// Horizon parameters
let v_horizon = sqrt(r * r - atmosphere.bottom_radius * atmosphere.bottom_radius);
let cos_beta = v_horizon / r;
let beta = fast_acos(cos_beta);
let beta = fast_acos_4(cos_beta);
let horizon_zenith = PI - beta;

var zenith: f32;
if adj_uv.y < 0.5 {
let coord = 1.0 - 2.0 * adj_uv.y;
zenith = horizon_zenith * (1.0 - coord * coord);
} else {
let coord = 2.0 * adj_uv.y - 1.0;
zenith = horizon_zenith + beta * coord * coord;
}
// Inverse of horizon-detail mapping to recover original latitude from texture coordinate
let t = abs(2.0 * (adj_uv.y - 0.5));
let l = sign(adj_uv.y - 0.5) * HALF_PI * t * t;

return vec2(zenith, azimuth);
return vec2(horizon_zenith - l, azimuth);
}

// LUT SAMPLING
Expand All @@ -127,13 +130,15 @@ fn sample_sky_view_lut(r: f32, ray_dir_as: vec3<f32>) -> vec3<f32> {
return textureSampleLevel(sky_view_lut, sky_view_lut_sampler, uv, 0.0).rgb;
}

//RGB channels: total inscattered light along the camera ray to the current sample.
//A channel: average transmittance across all wavelengths to the current sample.
// RGB channels: total inscattered light along the camera ray to the current sample.
// A channel: average transmittance across all wavelengths to the current sample.
fn sample_aerial_view_lut(uv: vec2<f32>, depth: f32) -> vec4<f32> {
let view_pos = view.view_from_clip * vec4(uv_to_ndc(uv), depth, 1.0);
let dist = length(view_pos.xyz / view_pos.w) * settings.scene_units_to_m;
let uvw = vec3(uv, dist / settings.aerial_view_lut_max_distance);
return textureSampleLevel(aerial_view_lut, aerial_view_lut_sampler, uvw, 0.0);
let sample = textureSampleLevel(aerial_view_lut, aerial_view_lut_sampler, uvw, 0.0);
// Recover the values from log space
return vec4(exp(sample.rgb), exp(-sample.a));
}

// PHASE FUNCTIONS
Expand Down Expand Up @@ -236,6 +241,9 @@ fn sample_local_inscattering(local_atmosphere: AtmosphereSample, ray_dir: vec3<f
const SUN_ANGULAR_SIZE: f32 = 0.0174533; // angular diameter of sun in radians

fn sample_sun_illuminance(ray_dir_ws: vec3<f32>, transmittance: vec3<f32>) -> vec3<f32> {
let r = view_radius();
let mu_view = ray_dir_ws.y;
let shadow_factor = f32(!ray_intersects_ground(r, mu_view));
var sun_illuminance = vec3(0.0);
for (var light_i: u32 = 0u; light_i < lights.n_directional_lights; light_i++) {
let light = &lights.directional_lights[light_i];
Expand All @@ -244,7 +252,7 @@ fn sample_sun_illuminance(ray_dir_ws: vec3<f32>, transmittance: vec3<f32>) -> ve
let pixel_size = fwidth(angle_to_sun);
let factor = smoothstep(0.0, -pixel_size * ROOT_2, angle_to_sun - SUN_ANGULAR_SIZE * 0.5);
let sun_solid_angle = (SUN_ANGULAR_SIZE * SUN_ANGULAR_SIZE) * 4.0 * FRAC_PI;
sun_illuminance += ((*light).color.rgb / sun_solid_angle) * factor * ray_dir_ws.y;
sun_illuminance += ((*light).color.rgb / sun_solid_angle) * factor * shadow_factor;
}
return sun_illuminance * transmittance * view.exposure;
}
Expand Down
15 changes: 5 additions & 10 deletions crates/bevy_pbr/src/atmosphere/sky_view_lut.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
sample_local_inscattering, get_local_r, view_radius,
max_atmosphere_distance, direction_atmosphere_to_world,
sky_view_lut_uv_to_zenith_azimuth, zenith_azimuth_to_ray_dir,
MIDPOINT_RATIO
},
}
}
Expand Down Expand Up @@ -36,19 +37,13 @@ fn main(@builtin(global_invocation_id) idx: vec3<u32>) {

// Raymarch with quadratic distribution
let sample_count = mix(1.0, f32(settings.sky_view_lut_samples), clamp(t_max * 0.01, 0.0, 1.0));
let sample_count_floor = floor(sample_count);
let t_max_floor = t_max * sample_count_floor / sample_count;
var total_inscattering = vec3(0.0);
var throughput = vec3(1.0);
var prev_t = 0.0;
for (var s = 0.0; s < sample_count; s += 1.0) {
// Use quadratic distribution like reference
var t0 = (s / sample_count_floor);
var t1 = ((s + 1.0) / sample_count_floor);
t0 = t0 * t0;
t1 = t1 * t1;
t1 = select(t_max_floor * t1, t_max, t1 > 1.0);
let t_i = t_max_floor * t0 + (t1 - t_max_floor * t0) * 0.3;
let dt_i = t1 - t_max_floor * t0;
let t_i = t_max * (s + MIDPOINT_RATIO) / sample_count;
let dt_i = (t_i - prev_t);
prev_t = t_i;

let local_r = get_local_r(r, mu, t_i);
let local_up = get_local_up(r, t_i, ray_dir_ws);
Expand Down
4 changes: 2 additions & 2 deletions crates/bevy_pbr/src/atmosphere/transmittance_lut.wgsl
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#import bevy_pbr::atmosphere::{
types::{Atmosphere, AtmosphereSettings},
bindings::{settings, atmosphere},
functions::{AtmosphereSample, sample_atmosphere, get_local_r, max_atmosphere_distance},
functions::{AtmosphereSample, sample_atmosphere, get_local_r, max_atmosphere_distance, MIDPOINT_RATIO},
bruneton_functions::{transmittance_lut_uv_to_r_mu, distance_to_bottom_atmosphere_boundary, distance_to_top_atmosphere_boundary},
}

Expand Down Expand Up @@ -32,7 +32,7 @@ fn ray_optical_depth(r: f32, mu: f32, sample_count: u32) -> vec3<f32> {
var prev_t = 0.0f;

for (var i = 0u; i < sample_count; i++) {
let t_i = t_max * (f32(i) + 0.3f) / f32(sample_count);
let t_i = t_max * (f32(i) + MIDPOINT_RATIO) / f32(sample_count);
let dt = t_i - prev_t;
prev_t = t_i;

Expand Down
4 changes: 3 additions & 1 deletion crates/bevy_render/src/maths.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ fn project_onto(lhs: vec3<f32>, rhs: vec3<f32>) -> vec3<f32> {
// accuracy can be sacrificed for greater sample count.

fn fast_sqrt(x: f32) -> f32 {
return bitcast<f32>(0x1fbd1df5 + (bitcast<i32>(x) >> 1u));
let n = bitcast<f32>(0x1fbd1df5 + (bitcast<i32>(x) >> 1u));
// One Newton's method iteration for better precision
return 0.5 * (n + x / n);
}

// Slightly less accurate than fast_acos_4, but much simpler.
Expand Down