diff --git a/Cargo.toml b/Cargo.toml index a371f316cbd57..856bcbe092eab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -102,41 +102,34 @@ unused_qualifications = "warn" [features] default = [ - "android-game-activity", - "android-game-activity", - "android_shared_stdcxx", "animation", "bevy_asset", + "bevy_state", "bevy_audio", "bevy_color", - "bevy_core_pipeline", "bevy_gilrs", - "bevy_gizmos", - "bevy_gltf", - "bevy_mesh_picking_backend", + "bevy_scene", + "bevy_winit", + "bevy_core_pipeline", "bevy_pbr", "bevy_picking", - "bevy_remote", + "bevy_gltf", "bevy_render", - "bevy_scene", "bevy_sprite", - "bevy_sprite_picking_backend", - "bevy_state", "bevy_text", "bevy_ui", - "bevy_ui_picking_backend", - "bevy_winit", - "custom_cursor", - "default_font", - "hdr", "multi_threaded", "png", - "smaa_luts", - "sysinfo_plugin", - "tonemapping_luts", + "hdr", "vorbis", - "webgl2", "x11", + "bevy_gizmos", + "android_shared_stdcxx", + "tonemapping_luts", + "smaa_luts", + "default_font", + "webgl2", + "sysinfo_plugin", ] # Provides an implementation for picking meshes @@ -202,7 +195,6 @@ bevy_sprite = [ "bevy_render", "bevy_core_pipeline", "bevy_color", - "bevy_sprite_picking_backend", ] # Provides text functionality @@ -215,7 +207,6 @@ bevy_ui = [ "bevy_text", "bevy_sprite", "bevy_color", - "bevy_ui_picking_backend", ] # winit window and input backend @@ -839,6 +830,17 @@ description = "A scene showcasing the atmospheric fog effect" category = "3D Rendering" wasm = true +[[example]] +name = "atmosphere" +path = "examples/3d/atmosphere.rs" +doc-scrape-examples = true + +[package.metadata.example.atmosphere] +name = "Atmosphere" +description = "A scene showcasing pbr atmospheric scattering" +category = "3D Rendering" +wasm = true + [[example]] name = "fog" path = "examples/3d/fog.rs" @@ -1219,7 +1221,7 @@ setup = [ "curl", "-o", "assets/models/bunny.meshlet_mesh", - "https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/defbd9b32072624d40d57de7d345c66a9edf5d0b/bunny.meshlet_mesh", + "https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/b6c712cfc87c65de419f856845401aba336a7bcd/bunny.meshlet_mesh", ], ] diff --git a/crates/bevy_pbr/src/atmosphere/aerial_view_lut.wgsl b/crates/bevy_pbr/src/atmosphere/aerial_view_lut.wgsl new file mode 100644 index 0000000000000..ef0fcf2100ffc --- /dev/null +++ b/crates/bevy_pbr/src/atmosphere/aerial_view_lut.wgsl @@ -0,0 +1,90 @@ +#import bevy_pbr::{ + mesh_view_types::{Lights, DirectionalLight}, + atmosphere::{ + types::{Atmosphere, AtmosphereSettings}, + bindings::{atmosphere, settings, view, lights, aerial_view_lut_out}, + functions::{ + 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, position_ndc_to_world, depth_ndc_to_view_z + }, + bruneton_functions::{distance_to_top_atmosphere_boundary, distance_to_bottom_atmosphere_boundary,ray_intersects_ground} + } +} + + +@group(0) @binding(12) var aerial_view_lut_out: texture_storage_3d; + +@compute +@workgroup_size(16, 16, 1) //TODO: this approach makes it so closer slices get fewer samples. But we also expect those to have less scattering. So win/win? +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); + let ray_dir = uv_to_ray_direction(uv); //TODO: negate for lighting calcs? + let r = view_radius(); + let mu = ray_dir.y; + + var prev_t = 0.0; + var total_inscattering = vec3(0.0); + var optical_depth = vec3(0.0); + for (var slice_i: i32 = i32(settings.aerial_view_lut_size.z - 1); slice_i >= 0; slice_i--) { //reversed loop to iterate raw depth values near->far + var sum_transmittance = 0.0; + for (var step_i: i32 = i32(settings.aerial_view_lut_samples - 1); step_i >= 0; step_i--) { //same here + let sample_depth = depth_at_sample(slice_i, step_i); + //view_dir.w is the cosine of the angle between the view vector and the camera forward vector, used to correct the step length. + let t_i = -depth_ndc_to_view_z(sample_depth) / ray_dir.w * settings.scene_units_to_km; + + let step_length = (t_i - prev_t); + prev_t = t_i; + + let local_r = get_local_r(r, mu, t_i); + if local_r > atmosphere.top_radius { break; } + let local_up = get_local_up(r, t_i, ray_dir.xyz); + + let local_atmosphere = sample_atmosphere(local_r); + optical_depth += local_atmosphere.extinction * step_length; //TODO: units between step_length and atmosphere + + let transmittance_to_sample = exp(-optical_depth); + + var local_inscattering = sample_local_inscattering(local_atmosphere, transmittance_to_sample, ray_dir.xyz, local_r, local_up); + total_inscattering += local_inscattering * step_length; + sum_transmittance += transmittance_to_sample.r + transmittance_to_sample.g + transmittance_to_sample.b; + } + let mean_transmittance = sum_transmittance / (f32(settings.aerial_view_lut_samples) * 3.0); + textureStore(aerial_view_lut_out, vec3(vec2(idx.xy), slice_i), vec4(total_inscattering, mean_transmittance)); + } +} + +fn depth_at_sample(slice_i: i32, step_i: i32) -> f32 { + return (f32(slice_i) + ((f32(step_i) + 0.5) / f32(settings.aerial_view_lut_samples))) / f32(settings.aerial_view_lut_size.z); +} + + +//Modified from skybox.wgsl. For this pass we don't need to apply a separate sky transform or consider camera viewport. +//w component is the cosine of the view direction with the view forward vector, to correct step distance at the edges of the viewport +fn uv_to_ray_direction(uv: vec2) -> vec4 { + // Using world positions of the fragment and camera to calculate a ray direction + // breaks down at large translations. This code only needs to know the ray direction. + // The ray direction is along the direction from the camera to the fragment position. + // In view space, the camera is at the origin, so the view space ray direction is + // along the direction of the fragment position - (0,0,0) which is just the + // fragment position. + // Use the position on the near clipping plane to avoid -inf world position + // because the far plane of an infinite reverse projection is at infinity. + let view_position_homogeneous = view.view_from_clip * vec4( + uv_to_ndc(uv), + 1.0, + 1.0, + ); + + let view_ray_direction = view_position_homogeneous.xyz / view_position_homogeneous.w; //TODO: remove this step and just use position_ndc_to_world? we didn't need to transform in view space + + // Transforming the view space ray direction by the inverse view matrix, transforms the + // direction to world space. Note that the w element is set to 0.0, as this is a + // vector direction, not a position, That causes the matrix multiplication to ignore + // the translations from the view matrix. + let ray_direction = (view.world_from_view * vec4(view_ray_direction, 0.0)).xyz; + + return vec4(normalize(ray_direction), -view_ray_direction.z); //TODO: correct sign? +} diff --git a/crates/bevy_pbr/src/atmosphere/bindings.wgsl b/crates/bevy_pbr/src/atmosphere/bindings.wgsl new file mode 100644 index 0000000000000..0081c6fabead4 --- /dev/null +++ b/crates/bevy_pbr/src/atmosphere/bindings.wgsl @@ -0,0 +1,21 @@ +#define_import_path bevy_pbr::atmosphere::bindings + +#import bevy_render::view::View; + +#import bevy_pbr::{ + mesh_view_types::Lights, + atmosphere::types::{Atmosphere, AtmosphereSettings} +} + +@group(0) @binding(0) var atmosphere: Atmosphere; +@group(0) @binding(1) var settings: AtmosphereSettings; +@group(0) @binding(2) var view: View; +@group(0) @binding(3) var lights: Lights; +@group(0) @binding(4) var transmittance_lut: texture_2d; +@group(0) @binding(5) var transmittance_lut_sampler: sampler; +@group(0) @binding(6) var multiscattering_lut: texture_2d; +@group(0) @binding(7) var multiscattering_lut_sampler: sampler; +@group(0) @binding(8) var sky_view_lut: texture_cube; +@group(0) @binding(9) var sky_view_lut_sampler: sampler; +@group(0) @binding(10) var aerial_view_lut: texture_3d; +@group(0) @binding(11) var aerial_view_lut_sampler: sampler; diff --git a/crates/bevy_pbr/src/atmosphere/bruneton_functions.wgsl b/crates/bevy_pbr/src/atmosphere/bruneton_functions.wgsl new file mode 100644 index 0000000000000..b7e0fc4e7cc08 --- /dev/null +++ b/crates/bevy_pbr/src/atmosphere/bruneton_functions.wgsl @@ -0,0 +1,139 @@ +// Copyright (c) 2017 Eric Bruneton +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +// +// Precomputed Atmospheric Scattering +// Copyright (c) 2008 INRIA +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. + +#define_import_path bevy_pbr::atmosphere::bruneton_functions + +#import bevy_pbr::atmosphere::{ + types::Atmosphere, + bindings::atmosphere, +} + +// Mapping from view height (r) and zenith cos angle (mu) to UV coordinates in the transmittance LUT +// Assuming r between ground and top atmosphere boundary, and mu= cos(zenith_angle) +// Chosen to increase precision near the ground and to work around a discontinuity at the horizon +// See Bruneton and Neyret 2008, "Precomputed Atmospheric Scattering" section 4 +fn transmittance_lut_r_mu_to_uv(r: f32, mu: f32) -> vec2 { + // Distance along a horizontal ray from the ground to the top atmosphere boundary + let H = sqrt(atmosphere.top_radius * atmosphere.top_radius - atmosphere.bottom_radius * atmosphere.bottom_radius); + + // Distance from a point at height r to the horizon + // ignore the case where r <= atmosphere.bottom_radius + let rho = sqrt(max(r * r - atmosphere.bottom_radius * atmosphere.bottom_radius, 0.0)); + + // Distance from a point at height r to the top atmosphere boundary at zenith angle mu + let d = distance_to_top_atmosphere_boundary(r, mu); + + // Minimum and maximum distance to the top atmosphere boundary from a point at height r + let d_min = atmosphere.top_radius - r; // length of the ray straight up to the top atmosphere boundary + let d_max = rho + H; // length of the ray to the top atmosphere boundary and grazing the horizon + + let u = (d - d_min) / (d_max - d_min); + let v = rho / H; + return vec2(u, v); +} + +// Inverse of the mapping above, mapping from UV coordinates in the transmittance LUT to view height (r) and zenith cos angle (mu) +fn transmittance_lut_uv_to_r_mu(uv: vec2) -> vec2 { + // Distance to top atmosphere boundary for a horizontal ray at ground level + let H = sqrt(atmosphere.top_radius * atmosphere.top_radius - atmosphere.bottom_radius * atmosphere.bottom_radius); + + // Distance to the horizon, from which we can compute r: + let rho = H * uv.y; + let r = sqrt(rho * rho + atmosphere.bottom_radius * atmosphere.bottom_radius); + + // Distance to the top atmosphere boundary for the ray (r,mu), and its minimum + // and maximum values over all mu- obtained for (r,1) and (r,mu_horizon) - + // from which we can recover mu: + let d_min = atmosphere.top_radius - r; + let d_max = rho + H; + let d = d_min + uv.x * (d_max - d_min); + + var mu: f32; + if d == 0.0 { + mu = 1.0; + } else { + mu = (H * H - rho * rho - d * d) / (2.0 * r * d); + } + + mu = clamp(mu, -1.0, 1.0); + + return vec2(r, mu); +} + +/// Simplified ray-sphere intersection +/// where: +/// Ray origin, o = [0,0,r] with r <= atmosphere.top_radius +/// mu is the cosine of spherical coordinate theta (-1.0 <= mu <= 1.0) +/// so ray direction in spherical coordinates is [1,acos(mu),0] which needs to be converted to cartesian +/// Direction of ray, u = [0,sqrt(1-mu*mu),mu] +/// Center of sphere, c = [0,0,0] +/// Radius of sphere, r = atmosphere.top_radius +/// This function solves the quadratic equation for line-sphere intersection simplified under these assumptions +fn distance_to_top_atmosphere_boundary(r: f32, mu: f32) -> f32 { + // ignore the case where r > atmosphere.top_radius + let positive_discriminant = max(r * r * (mu * mu - 1.0) + atmosphere.top_radius * atmosphere.top_radius, 0.0); + return max(-r * mu + sqrt(positive_discriminant), 0.0); +} + +/// Simplified ray-sphere intersection +/// as above for intersections with the ground +fn distance_to_bottom_atmosphere_boundary(r: f32, mu: f32) -> f32 { + let positive_discriminant = max(r * r * (mu * mu - 1.0) + atmosphere.bottom_radius * atmosphere.bottom_radius, 0.0); + return max(-r * mu - sqrt(positive_discriminant), 0.0); +} + +fn ray_intersects_ground(r: f32, mu: f32) -> bool { + return mu < 0.0 && r * r * (mu * mu - 1.0) + atmosphere.bottom_radius * atmosphere.bottom_radius >= 0.0; +} diff --git a/crates/bevy_pbr/src/atmosphere/functions.wgsl b/crates/bevy_pbr/src/atmosphere/functions.wgsl new file mode 100644 index 0000000000000..f111f2a6ad98a --- /dev/null +++ b/crates/bevy_pbr/src/atmosphere/functions.wgsl @@ -0,0 +1,230 @@ +#define_import_path bevy_pbr::atmosphere::functions + +#import bevy_pbr::atmosphere::{ + types::Atmosphere, + bindings::{ + atmosphere, settings, view, lights, transmittance_lut, transmittance_lut_sampler, + multiscattering_lut, multiscattering_lut_sampler, sky_view_lut, sky_view_lut_sampler, + aerial_view_lut, aerial_view_lut_sampler + }, + bruneton_functions::{transmittance_lut_r_mu_to_uv, transmittance_lut_uv_to_r_mu, ray_intersects_ground}, +} + +// CONSTANTS + +const PI: f32 = 3.141592653589793238462; +const TAU: f32 = 6.283185307179586476925; +const FRAC_PI: f32 = 0.31830988618379067153; // 1 / π +const FRAC_3_16_PI: f32 = 0.0596831036594607509; // 3 / (16π) +const FRAC_4_PI: f32 = 0.07957747154594767; // 1 / (4π) + +// LUT UV PARAMATERIZATIONS + +fn multiscattering_lut_r_mu_to_uv(r: f32, mu: f32) -> vec2 { + let u = 0.5 + 0.5 * mu; + let v = saturate((r - atmosphere.bottom_radius) / (atmosphere.top_radius - atmosphere.bottom_radius)); //TODO + return vec2(u, v); +} + +fn multiscattering_lut_uv_to_r_mu(uv: vec2) -> vec2 { + let r = mix(atmosphere.bottom_radius, atmosphere.top_radius, uv.y); + let mu = uv.x * 2 - 1; + return vec2(r, mu); +} + +fn sky_view_lut_squash_ray_dir(ray_dir_vs: vec3) -> vec3 { + let new_y = sqrt(abs(ray_dir_vs.y)) * sign(ray_dir_vs.y); + return normalize(vec3(ray_dir_vs.x, new_y, ray_dir_vs.z)); +} + +fn sky_view_lut_unsquash_ray_dir(ray_dir_vs: vec3) -> vec3 { + let abs_y = abs(ray_dir_vs.y); + let new_y = abs_y * abs_y * sign(ray_dir_vs.y); + return normalize(vec3(ray_dir_vs.x, new_y, ray_dir_vs.z)); +} + +// LUT SAMPLING + +fn sample_transmittance_lut(r: f32, mu: f32) -> vec3 { + let uv = transmittance_lut_r_mu_to_uv(r, mu); + return textureSampleLevel(transmittance_lut, transmittance_lut_sampler, uv, 0.0).rgb; +} + +fn sample_multiscattering_lut(r: f32, mu: f32) -> vec3 { + let uv = multiscattering_lut_r_mu_to_uv(r, mu); + return textureSampleLevel(multiscattering_lut, multiscattering_lut_sampler, uv, 0.0).rgb; +} + +fn sample_sky_view_lut(ray_dir_vs: vec3) -> vec3 { + let ray_dir_vs_squashed = sky_view_lut_squash_ray_dir(ray_dir_vs); + return textureSampleLevel(sky_view_lut, sky_view_lut_sampler, ray_dir_vs_squashed, 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. +fn sample_aerial_view_lut(ndc: vec3) -> vec4 { + return textureSampleLevel(aerial_view_lut, aerial_view_lut_sampler, vec3(ndc_to_uv(ndc.xy), ndc.z), 0.0); +} + +// PHASE FUNCTIONS + +fn rayleigh(neg_LdotV: f32) -> f32 { + return FRAC_3_16_PI * (1 + (neg_LdotV * neg_LdotV)); +} + +fn henyey_greenstein(neg_LdotV: f32) -> f32 { + let g = atmosphere.mie_asymmetry; + let denom = 1.0 + g * g - 2.0 * g * neg_LdotV; + return FRAC_4_PI * (1.0 - g * g) / (denom * sqrt(denom)); +} + +// ATMOSPHERE SAMPLING + +struct AtmosphereSample { + rayleigh_scattering: vec3, + mie_scattering: f32, + extinction: vec3 +} + +//prob fine to return big struct because of inlining +fn sample_atmosphere(r: f32) -> AtmosphereSample { + let altitude = r - atmosphere.bottom_radius; + + // atmosphere values at altitude + let mie_density = exp(atmosphere.mie_density_exp_scale * altitude); + let rayleigh_density = exp(atmosphere.rayleigh_density_exp_scale * altitude); + var ozone_density: f32 = max(0.0, 1.0 - (abs(altitude - atmosphere.ozone_layer_center_altitude) / atmosphere.ozone_layer_half_width)); + + let mie_scattering = mie_density * atmosphere.mie_scattering; + let mie_absorption = mie_density * atmosphere.mie_absorption; + let mie_extinction = mie_scattering + mie_absorption; + + let rayleigh_scattering = rayleigh_density * atmosphere.rayleigh_scattering; + // no rayleigh absorption + // rayleigh extinction is the sum of scattering and absorption + + // ozone doesn't contribute to scattering + let ozone_absorption = ozone_density * atmosphere.ozone_absorption; + + var sample: AtmosphereSample; + sample.rayleigh_scattering = rayleigh_scattering; + sample.mie_scattering = mie_scattering; + sample.extinction = rayleigh_scattering + mie_extinction + ozone_absorption; + + return sample; +} + + +fn sample_local_inscattering(local_atmosphere: AtmosphereSample, transmittance_to_sample: vec3, ray_dir: vec3, local_r: f32, local_up: vec3) -> vec3 { + var rayleigh_scattering = vec3(0.0); + var mie_scattering = vec3(0.0); + for (var light_i: u32 = 0u; light_i < lights.n_directional_lights; light_i++) { + let light = &lights.directional_lights[light_i]; + let mu_light = dot((*light).direction_to_light, local_up); + let neg_LdotV = dot((*light).direction_to_light, ray_dir); + let rayleigh_phase = rayleigh(neg_LdotV); + let mie_phase = henyey_greenstein(neg_LdotV); + + 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 psi_ms = sample_multiscattering_lut(local_r, mu_light); + + rayleigh_scattering += (transmittance_to_sample * shadow_factor * rayleigh_phase + psi_ms) * (*light).color.rgb; + mie_scattering += (transmittance_to_sample * shadow_factor * mie_phase + psi_ms) * (*light).color.rgb; + } + return (local_atmosphere.rayleigh_scattering * rayleigh_scattering + local_atmosphere.mie_scattering * mie_scattering) * view.exposure; +} + +//TODO: make pr to specify light angular size on struct itself +const SUN_ANGULAR_SIZE: f32 = 0.00436332; //angular radius of sun in radians +//const SUN_ANGULAR_SIZE: f32 = 0.1; + +fn sample_sun_disk(ray_dir: vec3, transmittance: vec3) -> vec3 { + var sun_contribution = vec3(0.0); + for (var light_i: u32 = 0u; light_i < lights.n_directional_lights; light_i++) { + let light = &lights.directional_lights[light_i]; + let neg_LdotV = dot((*light).direction_to_light, ray_dir); + let angle_to_light = acos(neg_LdotV); + sun_contribution += (*light).color.rgb * f32(angle_to_light <= SUN_ANGULAR_SIZE); + } + return sun_contribution * transmittance * view.exposure; +} + +// TRANSFORM UTILITIES + +fn view_radius() -> f32 { + return view.world_position.y * settings.scene_units_to_km + atmosphere.bottom_radius; +} + +//We assume the `up` vector at the view position is the y axis, since the world is locally flat/level. +//t = distance along view ray (km) +//NOTE: this means that if your world is actually spherical, this will be wrong. +fn get_local_up(r: f32, t: f32, ray_dir: vec3) -> vec3 { + return normalize(vec3(0.0, r, 0.0) + t * ray_dir); +} + +//given a ray starting at radius r, with cos(zenith angle) of mu, +//and a distance along the ray of t, gives the new radius at point t +fn get_local_r(r: f32, mu: f32, t: f32) -> f32 { + return sqrt(t * t + 2.0 * r * mu * t + r * r); +} + +// Convert uv [0.0 .. 1.0] coordinate to ndc space xy [-1.0 .. 1.0] +fn uv_to_ndc(uv: vec2) -> vec2 { + return uv * vec2(2.0, -2.0) + vec2(-1.0, 1.0); +} + +/// Convert ndc space xy coordinate [-1.0 .. 1.0] to uv [0.0 .. 1.0] +fn ndc_to_uv(ndc: vec2) -> vec2 { + return ndc * vec2(0.5, -0.5) + vec2(0.5); +} + +/// Convert a ndc space position to world space +fn position_ndc_to_world(ndc_pos: vec3) -> vec3 { + let world_pos = view.world_from_clip * vec4(ndc_pos, 1.0); + return world_pos.xyz / world_pos.w; +} + +//Modified from skybox.wgsl. For this pass we don't need to apply a separate sky transform or consider camera viewport. +//w component is the cosine of the view direction with the view forward vector, to correct step distance at the edges of the viewport +fn uv_to_ray_direction(uv: vec2) -> vec4 { + // Using world positions of the fragment and camera to calculate a ray direction + // breaks down at large translations. This code only needs to know the ray direction. + // The ray direction is along the direction from the camera to the fragment position. + // In view space, the camera is at the origin, so the view space ray direction is + // along the direction of the fragment position - (0,0,0) which is just the + // fragment position. + // Use the position on the near clipping plane to avoid -inf world position + // because the far plane of an infinite reverse projection is at infinity. + let view_position_homogeneous = view.view_from_clip * vec4( + uv_to_ndc(uv), + 1.0, + 1.0, + ); + + // Transforming the view space ray direction by the skybox transform matrix, it is + // equivalent to rotating the skybox itself. + let view_ray_direction = view_position_homogeneous.xyz / view_position_homogeneous.w; //TODO: remove this step and just use position_ndc_to_world? we didn't need to transform in view space + + // Transforming the view space ray direction by the inverse view matrix, transforms the + // direction to world space. Note that the w element is set to 0.0, as this is a + // vector direction, not a position, That causes the matrix multiplication to ignore + // the translations from the view matrix. + let ray_direction = (view.world_from_view * vec4(view_ray_direction, 0.0)).xyz; + + return vec4(normalize(ray_direction), -view_ray_direction.z); +} + +/// Convert a view space direction to world space +fn direction_view_to_world(view_dir: vec3) -> vec3 { + let world_dir = view.world_from_view * vec4(view_dir, 0.0); + return world_dir.xyz; +} + +/// Convert ndc depth to linear view z. +/// Note: Depth values in front of the camera will be negative as -z is forward +fn depth_ndc_to_view_z(ndc_depth: f32) -> f32 { + let view_pos = view.view_from_clip * vec4(0.0, 0.0, ndc_depth, 1.0); + return view_pos.z / view_pos.w; +} diff --git a/crates/bevy_pbr/src/atmosphere/mod.rs b/crates/bevy_pbr/src/atmosphere/mod.rs new file mode 100644 index 0000000000000..e330f3ca61ecc --- /dev/null +++ b/crates/bevy_pbr/src/atmosphere/mod.rs @@ -0,0 +1,280 @@ +mod node; +pub mod resources; + +use bevy_app::{App, Plugin}; +use bevy_asset::load_internal_asset; +use bevy_core_pipeline::core_3d::graph::Node3d; +use bevy_ecs::{ + component::Component, + query::{QueryItem, With}, + schedule::IntoSystemConfigs, + system::lifetimeless::Read, +}; +use bevy_math::{UVec2, UVec3, Vec3}; +use bevy_reflect::Reflect; +use bevy_render::{extract_component::UniformComponentPlugin, render_resource::ShaderType}; +use bevy_render::{ + extract_component::{ExtractComponent, ExtractComponentPlugin}, + render_graph::{RenderGraphApp, ViewNodeRunner}, + render_resource::{Shader, TextureFormat, TextureUsages}, + renderer::RenderAdapter, + Render, RenderApp, RenderSet, +}; +use bevy_utils::tracing::warn; + +use bevy_core_pipeline::core_3d::{graph::Core3d, Camera3d}; + +use self::{ + node::{AtmosphereLutsNode, AtmosphereNode, RenderSkyNode}, + resources::{ + prepare_atmosphere_bind_groups, prepare_atmosphere_textures, AtmosphereBindGroupLayouts, + AtmospherePipelines, AtmosphereSamplers, + }, +}; + +mod shaders { + use bevy_asset::Handle; + use bevy_render::render_resource::Shader; + + pub const TYPES: Handle = Handle::weak_from_u128(0xB4CA686B10FA592B508580CCC2F9558C); + pub const FUNCTIONS: Handle = + Handle::weak_from_u128(0xD5524FD88BDC153FBF256B7F2C21906F); + pub const BRUNETON_FUNCTIONS: Handle = + Handle::weak_from_u128(0x7E896F48B707555DD11985F9C1594459); + pub const BINDINGS: Handle = Handle::weak_from_u128(0x140EFD89B5D4C8490AB895010DFC42FE); + + pub const TRANSMITTANCE_LUT: Handle = + Handle::weak_from_u128(0xEECBDEDFEED7F4EAFBD401BFAA5E0EFB); + pub const MULTISCATTERING_LUT: Handle = + Handle::weak_from_u128(0x65915B32C44B6287C0CCE1E70AF2936A); + pub const SKY_VIEW_LUT: Handle = + Handle::weak_from_u128(0x54136D7E6FFCD45BE38399A4E5ED7186); + pub const AERIAL_VIEW_LUT: Handle = + Handle::weak_from_u128(0x6FDEC284AD356B78C3A4D8ED4CBA0BC5); + pub const RENDER_SKY: Handle = + Handle::weak_from_u128(0x1951EB87C8A6129F0B541B1E4B3D4962); +} + +pub struct AtmospherePlugin; + +impl Plugin for AtmospherePlugin { + fn build(&self, app: &mut App) { + load_internal_asset!(app, shaders::TYPES, "types.wgsl", Shader::from_wgsl); + load_internal_asset!(app, shaders::FUNCTIONS, "functions.wgsl", Shader::from_wgsl); + load_internal_asset!( + app, + shaders::BRUNETON_FUNCTIONS, + "bruneton_functions.wgsl", + Shader::from_wgsl + ); + + load_internal_asset!(app, shaders::BINDINGS, "bindings.wgsl", Shader::from_wgsl); + + load_internal_asset!( + app, + shaders::TRANSMITTANCE_LUT, + "transmittance_lut.wgsl", + Shader::from_wgsl + ); + + load_internal_asset!( + app, + shaders::MULTISCATTERING_LUT, + "multiscattering_lut.wgsl", + Shader::from_wgsl + ); + + load_internal_asset!( + app, + shaders::SKY_VIEW_LUT, + "sky_view_lut.wgsl", + Shader::from_wgsl + ); + + load_internal_asset!( + app, + shaders::AERIAL_VIEW_LUT, + "aerial_view_lut.wgsl", + Shader::from_wgsl + ); + + load_internal_asset!( + app, + shaders::RENDER_SKY, + "render_sky.wgsl", + Shader::from_wgsl + ); + + app.register_type::() + .register_type::() + .add_plugins(( + ExtractComponentPlugin::::default(), + ExtractComponentPlugin::::default(), + UniformComponentPlugin::::default(), + UniformComponentPlugin::::default(), + )); + } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + if !render_app + .world() + .resource::() + .get_texture_format_features(TextureFormat::Rgba16Float) + .allowed_usages + .contains(TextureUsages::STORAGE_BINDING) + { + warn!("AtmospherePlugin not loaded. GPU lacks support: TextureFormat::Rgba16Float does not support TextureUsages::STORAGE_BINDING."); + return; + } + + render_app + .init_resource::() + .init_resource::() + .init_resource::() + .add_systems( + Render, + ( + prepare_atmosphere_textures.in_set(RenderSet::PrepareResources), + prepare_atmosphere_bind_groups.in_set(RenderSet::PrepareBindGroups), + ), + ) + .add_render_graph_node::>( + Core3d, + AtmosphereNode::RenderLuts, + ) + .add_render_graph_edges( + Core3d, + ( + // END_PRE_PASSES -> RENDER_LUTS -> MAIN_PASS + Node3d::EndPrepasses, + AtmosphereNode::RenderLuts, + Node3d::StartMainPass, + ), + ) + .add_render_graph_node::>( + Core3d, + AtmosphereNode::RenderSky, + ) + .add_render_graph_edges( + Core3d, + ( + Node3d::MainOpaquePass, + AtmosphereNode::RenderSky, + Node3d::MainTransparentPass, + ), + ); + } +} + +/// This component describes the atmosphere of a planet +//TODO: padding/alignment? +#[derive(Clone, Component, Reflect, ShaderType)] +#[require(AtmosphereSettings)] +pub struct Atmosphere { + /// Radius of the planet + /// + /// units: km + pub bottom_radius: f32, + + /// Radius at which we consider the atmosphere to 'end' for out calculations (from center of planet) + /// units: km + pub top_radius: f32, + + pub ground_albedo: Vec3, //used for estimating multiscattering + + pub rayleigh_density_exp_scale: f32, + pub rayleigh_scattering: Vec3, + + pub mie_density_exp_scale: f32, + pub mie_scattering: f32, //units: km^-1 + pub mie_absorption: f32, //units: km^-1 + pub mie_asymmetry: f32, //the "asymmetry" value of the phase function, unitless. Domain: (-1, 1) + + pub ozone_layer_center_altitude: f32, //units: km + pub ozone_layer_half_width: f32, //units: km + pub ozone_absorption: Vec3, //ozone absorption. units: km^-1 +} + +impl Atmosphere { + //TODO: check all these values before merge + //TODO: UNITS + pub const EARTH: Atmosphere = Atmosphere { + bottom_radius: 6360.0, + top_radius: 6460.0, + ground_albedo: Vec3::splat(0.3), + rayleigh_density_exp_scale: -1.0 / 8.0, + rayleigh_scattering: Vec3::new(0.005802, 0.013558, 0.033100), + mie_density_exp_scale: -1.0 / 1.2, + mie_scattering: 0.003996, + mie_absorption: 0.004440, + mie_asymmetry: 0.8, + ozone_layer_center_altitude: 25.0, + ozone_layer_half_width: 15.0, + ozone_absorption: Vec3::new(0.000650, 0.001881, 0.000085), + }; +} + +impl Default for Atmosphere { + fn default() -> Self { + Self::EARTH + } +} + +impl ExtractComponent for Atmosphere { + type QueryData = Read; + + type QueryFilter = With; + + type Out = Atmosphere; + + fn extract_component(item: QueryItem<'_, Self::QueryData>) -> Option { + Some(item.clone()) + } +} + +#[derive(Clone, Component, Reflect, ShaderType)] +pub struct AtmosphereSettings { + pub transmittance_lut_size: UVec2, + pub multiscattering_lut_size: UVec2, + pub aerial_view_lut_size: UVec3, + pub sky_view_lut_size: u32, + pub multiscattering_lut_dirs: u32, + pub transmittance_lut_samples: u32, + pub multiscattering_lut_samples: u32, + pub sky_view_lut_samples: u32, + pub aerial_view_lut_samples: u32, + pub scene_units_to_km: f32, +} + +impl Default for AtmosphereSettings { + fn default() -> Self { + Self { + transmittance_lut_size: UVec2::new(256, 128), + transmittance_lut_samples: 40, + multiscattering_lut_size: UVec2::new(32, 32), + multiscattering_lut_dirs: 64, + multiscattering_lut_samples: 20, + sky_view_lut_size: 64, + sky_view_lut_samples: 30, + aerial_view_lut_size: UVec3::new(32, 32, 32), + aerial_view_lut_samples: 10, + scene_units_to_km: 1.0e-3, + } + } +} + +impl ExtractComponent for AtmosphereSettings { + type QueryData = Read; + + type QueryFilter = (With, With); + + type Out = AtmosphereSettings; + + fn extract_component(item: QueryItem<'_, Self::QueryData>) -> Option { + Some(item.clone()) + } +} diff --git a/crates/bevy_pbr/src/atmosphere/multiscattering_lut.wgsl b/crates/bevy_pbr/src/atmosphere/multiscattering_lut.wgsl new file mode 100644 index 0000000000000..bb9fa26ef6dc0 --- /dev/null +++ b/crates/bevy_pbr/src/atmosphere/multiscattering_lut.wgsl @@ -0,0 +1,56 @@ +#import bevy_pbr::{ + mesh_view_types::{Lights, DirectionalLight}, + atmosphere::{ + types::{Atmosphere, AtmosphereSettings}, + bindings::{atmosphere, settings}, + functions::{multiscattering_lut_uv_to_r_mu, sample_transmittance_lut}, + bruneton_functions::{ + distance_to_top_atmosphere_boundary, distance_to_bottom_atmosphere_boundary, + } + } +} + + +const PHI_2: vec2 = vec2(1.3247179572447460259609088, 1.7548776662466927600495087); + +@group(0) @binding(12) var multiscattering_lut_out: texture_storage_2d; + +fn s2_sequence(n: u32) -> vec2 { + return fract(0.5 + f32(n) * PHI_2); +} + +//Lambert equal-area projection. +fn map_to_hemisphere(uv: vec2) -> vec2 { + //NOTE: must make sure to map to the a hemisphere centered on +-Z, + //since the integral is symmetric about the x axis + return vec2(0.0, 0.0); //TODO +} + +@compute +@workgroup_size(16, 16, 1) +fn main(@builtin(global_invocation_id) global_id: vec3) { + let uv: vec2 = (vec2(global_id.xy) + 0.5) / vec2(settings.multiscattering_lut_size); + + //See Multiscattering LUT paramatrization + //let r_mu = multiscattering_lut_uv_to_r_mu(uv); + + //single directional light is oriented exactly along the x axis, + //with an zenith angle corresponding to mu + //let direction_to_light = normalize(vec3(1.0, r_mu.y, 0.0)); + + /*for (var dir_i: u32= 0u; dir_i < settings.multiscattering_lut_dirs; dir_i++) { + let phi_theta = map_to_hemisphere(s2_sequence(dir_i)); + let mu = phi_theta.y; // cos(zenith_angle) = dot(vec3::up, dir); + + let atmosphere_dist = min(top_atmosphere_dist, bottom_atmosphere_dist); + + sample_multiscattering_dir(atmosphere, r_mu, atmosphere_dist); + }*/ +} + + +fn sample_multiscattering_dir(atmosphere: Atmosphere, r: f32, mu: f32, dir: vec2, atmosphere_dist: f32) { + for (var step_i: u32 = 0u; step_i < settings.multiscattering_lut_samples; step_i++) { + } +} + diff --git a/crates/bevy_pbr/src/atmosphere/node.rs b/crates/bevy_pbr/src/atmosphere/node.rs new file mode 100644 index 0000000000000..c03eea0f25a84 --- /dev/null +++ b/crates/bevy_pbr/src/atmosphere/node.rs @@ -0,0 +1,256 @@ +use bevy_ecs::{query::QueryItem, system::lifetimeless::Read, world::World}; +use bevy_render::{ + extract_component::DynamicUniformIndex, + render_graph::{NodeRunError, RenderGraphContext, RenderLabel, ViewNode}, + render_resource::{ + ComputePassDescriptor, LoadOp, Operations, PipelineCache, RenderPassColorAttachment, + RenderPassDescriptor, StoreOp, + }, + renderer::RenderContext, + view::{ViewTarget, ViewUniformOffset}, +}; + +use crate::ViewLightsUniformOffset; + +use super::{ + resources::{AtmosphereBindGroups, AtmospherePipelines, AtmosphereTextures}, + Atmosphere, AtmosphereSettings, +}; + +#[derive(PartialEq, Eq, Debug, Copy, Clone, Hash, RenderLabel)] +pub enum AtmosphereNode { + RenderLuts, + RenderSky, +} + +#[derive(Default)] +pub(super) struct AtmosphereLutsNode {} + +impl ViewNode for AtmosphereLutsNode { + type ViewQuery = ( + Read, + Read, + Read, + Read>, + Read>, + Read, + Read, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + ( + textures, + settings, + bind_groups, + atmosphere_uniforms_offset, + settings_uniforms_offset, + view_uniforms_offset, + lights_uniforms_offset, + ): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + let pipelines = world.resource::(); + let pipeline_cache = world.resource::(); + let ( + Some(transmittance_lut_pipeline), + Some(multiscattering_lut_pipeline), + Some(sky_view_lut_pipeline), + Some(aerial_view_lut_pipeline), + ) = ( + pipeline_cache.get_render_pipeline(pipelines.transmittance_lut), + pipeline_cache.get_compute_pipeline(pipelines.multiscattering_lut), + pipeline_cache.get_compute_pipeline(pipelines.sky_view_lut), + pipeline_cache.get_compute_pipeline(pipelines.aerial_view_lut), + ) + else { + //TODO: warning + return Ok(()); + }; + + let commands = render_context.command_encoder(); + + commands.push_debug_group("atmosphere_luts"); + + { + let mut transmittance_lut_pass = commands.begin_render_pass(&RenderPassDescriptor { + label: Some("transmittance_lut_pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: &textures.transmittance_lut.default_view, + resolve_target: None, + ops: Operations::default(), + })], + depth_stencil_attachment: None, + ..Default::default() + }); + transmittance_lut_pass.set_pipeline(transmittance_lut_pipeline); + transmittance_lut_pass.set_bind_group( + 0, + &bind_groups.transmittance_lut, + &[ + atmosphere_uniforms_offset.index(), + settings_uniforms_offset.index(), + ], + ); + transmittance_lut_pass.draw(0..3, 0..1); + } + + //todo: use fragment shader here? maybe shared memory would be nice though + { + let mut multiscattering_lut_pass = + commands.begin_compute_pass(&ComputePassDescriptor { + label: Some("multiscatttering_lut_pass"), + timestamp_writes: None, + }); + multiscattering_lut_pass.set_pipeline(multiscattering_lut_pipeline); + multiscattering_lut_pass.set_bind_group( + 0, + &bind_groups.multiscattering_lut, + &[ + atmosphere_uniforms_offset.index(), + settings_uniforms_offset.index(), + ], + ); + + const MULTISCATTERING_WORKGROUP_SIZE: u32 = 16; + let workgroups_x = settings + .multiscattering_lut_size + .x + .div_ceil(MULTISCATTERING_WORKGROUP_SIZE); + let workgroups_y = settings + .multiscattering_lut_size + .y + .div_ceil(MULTISCATTERING_WORKGROUP_SIZE); + + multiscattering_lut_pass.dispatch_workgroups(workgroups_x, workgroups_y, 1); + } + + const SKY_VIEW_WORKGROUP_SIZE: u32 = 16; + let workgroups_x = settings.sky_view_lut_size.div_ceil(SKY_VIEW_WORKGROUP_SIZE); + let workgroups_y = settings.sky_view_lut_size.div_ceil(SKY_VIEW_WORKGROUP_SIZE); + + { + let mut sky_view_lut_pass = commands.begin_compute_pass(&ComputePassDescriptor { + label: Some("sky_view_lut_pass"), + timestamp_writes: None, + }); + sky_view_lut_pass.set_pipeline(sky_view_lut_pipeline); + sky_view_lut_pass.set_bind_group( + 0, + &bind_groups.sky_view_lut, + &[ + atmosphere_uniforms_offset.index(), + settings_uniforms_offset.index(), + view_uniforms_offset.offset, + lights_uniforms_offset.offset, + ], + ); + sky_view_lut_pass.dispatch_workgroups(workgroups_x, workgroups_y, 6); + } + + { + let mut aerial_view_lut_pass = commands.begin_compute_pass(&ComputePassDescriptor { + label: Some("aerial_view_lut_pass"), + timestamp_writes: None, + }); + aerial_view_lut_pass.set_pipeline(aerial_view_lut_pipeline); + aerial_view_lut_pass.set_bind_group( + 0, + &bind_groups.aerial_view_lut, + &[ + atmosphere_uniforms_offset.index(), + settings_uniforms_offset.index(), + view_uniforms_offset.offset, + lights_uniforms_offset.offset, + ], + ); + + const AERIAL_VIEW_WORKGROUP_SIZE: u32 = 16; + let workgroups_x = settings + .aerial_view_lut_size + .x + .div_ceil(AERIAL_VIEW_WORKGROUP_SIZE); + let workgroups_y = settings + .aerial_view_lut_size + .y + .div_ceil(AERIAL_VIEW_WORKGROUP_SIZE); + + aerial_view_lut_pass.dispatch_workgroups(workgroups_x, workgroups_y, 1); + } + + render_context.command_encoder().pop_debug_group(); + Ok(()) + } +} + +#[derive(Default)] +pub(super) struct RenderSkyNode; + +impl ViewNode for RenderSkyNode { + type ViewQuery = ( + Read, + Read, + Read>, + Read>, + Read, + Read, + ); + + fn run<'w>( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + ( + atmosphere_bind_groups, + view_target, + atmosphere_uniforms_offset, + settings_uniforms_offset, + view_uniforms_offset, + lights_uniforms_offset, + ): QueryItem<'w, Self::ViewQuery>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let atmosphere_pipelines = world.resource::(); + let Some(render_sky_pipeline) = + pipeline_cache.get_render_pipeline(atmosphere_pipelines.render_sky) + else { + return Ok(()); + }; //TODO: warning + + let mut render_sky_pass = + render_context + .command_encoder() + .begin_render_pass(&RenderPassDescriptor { + label: Some("render_sky_pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: view_target.main_texture_view(), + resolve_target: None, + ops: Operations { + load: LoadOp::Load, + store: StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + render_sky_pass.set_pipeline(render_sky_pipeline); + render_sky_pass.set_bind_group( + 0, + &atmosphere_bind_groups.render_sky, + &[ + atmosphere_uniforms_offset.index(), + settings_uniforms_offset.index(), + view_uniforms_offset.offset, + lights_uniforms_offset.offset, + ], + ); + render_sky_pass.draw(0..3, 0..1); + + Ok(()) + } +} diff --git a/crates/bevy_pbr/src/atmosphere/render_sky.wgsl b/crates/bevy_pbr/src/atmosphere/render_sky.wgsl new file mode 100644 index 0000000000000..ce778a9f1ecbe --- /dev/null +++ b/crates/bevy_pbr/src/atmosphere/render_sky.wgsl @@ -0,0 +1,54 @@ +#import bevy_pbr::atmosphere::{ + types::{Atmosphere, AtmosphereSettings}, + bindings::{atmosphere, view}, + functions::{sample_transmittance_lut, sample_sky_view_lut, direction_view_to_world, uv_to_ndc, sample_aerial_view_lut, view_radius, sample_sun_disk}, +}; +#import bevy_render::view::View; + +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput + +@group(0) @binding(12) var depth_texture: texture_depth_2d; + +@fragment +fn main(in: FullscreenVertexOutput) -> @location(0) vec4 { + let depth = textureLoad(depth_texture, vec2(in.position.xy), 0); + if depth == 0.0 { + let view_ray_dir = uv_to_ray_direction(in.uv).xyz; + let world_ray_dir = direction_view_to_world(view_ray_dir); + let r = view_radius(); + let mu = world_ray_dir.y; + let sky_view = sample_sky_view_lut(view_ray_dir); + let transmittance = sample_transmittance_lut(r, mu); + let sun_disk = sample_sun_disk(world_ray_dir, transmittance); + return vec4(sky_view + sun_disk, (transmittance.r + transmittance.g + transmittance.b) / 3.0); + } else { + let ndc_xy = uv_to_ndc(in.uv); + let ndc = vec3(ndc_xy, depth); + let inscattering = sample_aerial_view_lut(ndc); + return inscattering; + } +} + +//Modified from skybox.wgsl. For this pass we don't need to apply a separate sky transform or consider camera viewport. +//w component is the cosine of the view direction with the view forward vector, to correct step distance at the edges of the viewport +fn uv_to_ray_direction(uv: vec2) -> vec4 { + // Using world positions of the fragment and camera to calculate a ray direction + // breaks down at large translations. This code only needs to know the ray direction. + // The ray direction is along the direction from the camera to the fragment position. + // In view space, the camera is at the origin, so the view space ray direction is + // along the direction of the fragment position - (0,0,0) which is just the + // fragment position. + // Use the position on the near clipping plane to avoid -inf world position + // because the far plane of an infinite reverse projection is at infinity. + let view_position_homogeneous = view.view_from_clip * vec4( + uv_to_ndc(uv), + 1.0, + 1.0, + ); + + // Transforming the view space ray direction by the skybox transform matrix, it is + // equivalent to rotating the skybox itself. + let view_ray_direction = view_position_homogeneous.xyz / view_position_homogeneous.w; //TODO: remove this step and just use position_ndc_to_world? we didn't need to transform in view space + + return vec4(normalize(view_ray_direction), -view_ray_direction.z); +} diff --git a/crates/bevy_pbr/src/atmosphere/resources.rs b/crates/bevy_pbr/src/atmosphere/resources.rs new file mode 100644 index 0000000000000..ba3129c9e1781 --- /dev/null +++ b/crates/bevy_pbr/src/atmosphere/resources.rs @@ -0,0 +1,555 @@ +use bevy_core_pipeline::{ + core_3d::Camera3d, fullscreen_vertex_shader::fullscreen_shader_vertex_state, +}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::With, + system::{Commands, Query, Res, ResMut, Resource}, + world::{FromWorld, World}, +}; +use bevy_render::{ + extract_component::ComponentUniforms, + render_resource::{ + binding_types::{ + sampler, texture_2d, texture_3d, texture_cube, texture_storage_2d, + texture_storage_2d_array, texture_storage_3d, uniform_buffer, + }, + AddressMode, BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, + BlendComponent, BlendFactor, BlendOperation, BlendState, CachedComputePipelineId, + CachedRenderPipelineId, ColorTargetState, ColorWrites, ComputePipelineDescriptor, Extent3d, + FilterMode, FragmentState, MultisampleState, PipelineCache, PrimitiveState, + RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, ShaderStages, + StorageTextureAccess, TextureDescriptor, TextureDimension, TextureFormat, + TextureSampleType, TextureUsages, TextureView, TextureViewDescriptor, TextureViewDimension, + }, + renderer::RenderDevice, + texture::{CachedTexture, TextureCache}, + view::{ViewDepthTexture, ViewUniform, ViewUniforms}, +}; + +use crate::{GpuLights, LightMeta}; + +use super::{shaders, Atmosphere, AtmosphereSettings}; + +#[derive(Resource)] +pub(crate) struct AtmosphereBindGroupLayouts { + pub transmittance_lut: BindGroupLayout, + pub multiscattering_lut: BindGroupLayout, + pub sky_view_lut: BindGroupLayout, + pub aerial_view_lut: BindGroupLayout, + pub render_sky: BindGroupLayout, +} + +impl FromWorld for AtmosphereBindGroupLayouts { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + let transmittance_lut = render_device.create_bind_group_layout( + "transmittance_lut_bind_group_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::FRAGMENT, + ( + (0, uniform_buffer::(true)), + (1, uniform_buffer::(true)), + ), + ), + ); + + let multiscattering_lut = render_device.create_bind_group_layout( + "multiscattering_lut_bind_group_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::COMPUTE, + ( + (0, uniform_buffer::(true)), + (1, uniform_buffer::(true)), + (4, texture_2d(TextureSampleType::Float { filterable: true })), //transmittance lut and sampler + (5, sampler(SamplerBindingType::Filtering)), + ( + //multiscattering lut storage texture + 12, + texture_storage_2d( + TextureFormat::Rgba16Float, + StorageTextureAccess::WriteOnly, + ), + ), + ), + ), + ); + + let sky_view_lut = render_device.create_bind_group_layout( + "sky_view_lut_bind_group_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::COMPUTE, + ( + (0, uniform_buffer::(true)), + (1, uniform_buffer::(true)), + (2, uniform_buffer::(true)), + (3, uniform_buffer::(true)), + (4, texture_2d(TextureSampleType::Float { filterable: true })), //transmittance lut and sampler + (5, sampler(SamplerBindingType::Filtering)), + (6, texture_2d(TextureSampleType::Float { filterable: true })), //multiscattering lut and sampler + (7, sampler(SamplerBindingType::Filtering)), + ( + 12, + texture_storage_2d_array( + TextureFormat::Rgba16Float, + StorageTextureAccess::WriteOnly, + ), + ), + ), + ), + ); + + let aerial_view_lut = render_device.create_bind_group_layout( + "aerial_view_lut_bind_group_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::COMPUTE, + ( + (0, uniform_buffer::(true)), + (1, uniform_buffer::(true)), + (2, uniform_buffer::(true)), + (3, uniform_buffer::(true)), + (4, texture_2d(TextureSampleType::Float { filterable: true })), //transmittance lut and sampler + (5, sampler(SamplerBindingType::Filtering)), + (6, texture_2d(TextureSampleType::Float { filterable: true })), //multiscattering lut and sampler + (7, sampler(SamplerBindingType::Filtering)), + ( + //Aerial view lut storage texture + 12, + texture_storage_3d( + TextureFormat::Rgba16Float, + StorageTextureAccess::WriteOnly, + ), + ), + ), + ), + ); + + let render_sky = render_device.create_bind_group_layout( + "render_sky_bind_group_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::FRAGMENT, + ( + (0, uniform_buffer::(true)), + (1, uniform_buffer::(true)), + (2, uniform_buffer::(true)), + (3, uniform_buffer::(true)), + (4, texture_2d(TextureSampleType::Float { filterable: true })), //transmittance lut and sampler + (5, sampler(SamplerBindingType::Filtering)), + ( + //sky view lut and sampler + 8, + texture_cube(TextureSampleType::Float { filterable: true }), + ), + (9, sampler(SamplerBindingType::Filtering)), + ( + // aerial view lut and sampler + 10, + texture_3d(TextureSampleType::Float { filterable: true }), + ), + (11, sampler(SamplerBindingType::Filtering)), + ( + //view depth texture + 12, + texture_2d(TextureSampleType::Depth), + ), + ), + ), + ); + + Self { + transmittance_lut, + multiscattering_lut, + sky_view_lut, + aerial_view_lut, + render_sky, + } + } +} + +#[derive(Resource)] +pub struct AtmosphereSamplers { + pub transmittance_lut: Sampler, + pub multiscattering_lut: Sampler, + pub sky_view_lut: Sampler, + pub aerial_view_lut: Sampler, +} + +impl FromWorld for AtmosphereSamplers { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + + let base_sampler = SamplerDescriptor { + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + mipmap_filter: FilterMode::Nearest, + ..Default::default() + }; + + let transmittance_lut = render_device.create_sampler(&SamplerDescriptor { + label: Some("transmittance_lut_sampler"), + ..base_sampler + }); + + let multiscattering_lut = render_device.create_sampler(&SamplerDescriptor { + label: Some("multiscattering_lut_sampler"), + ..base_sampler + }); + + let sky_view_lut = render_device.create_sampler(&SamplerDescriptor { + label: Some("sky_view_lut_sampler"), + ..base_sampler + }); + + let aerial_view_lut = render_device.create_sampler(&SamplerDescriptor { + label: Some("aerial_view_lut_sampler"), + ..base_sampler + }); + + Self { + transmittance_lut, + multiscattering_lut, + sky_view_lut, + aerial_view_lut, + } + } +} + +#[derive(Resource)] +pub(crate) struct AtmospherePipelines { + pub transmittance_lut: CachedRenderPipelineId, + pub multiscattering_lut: CachedComputePipelineId, + pub sky_view_lut: CachedComputePipelineId, + pub aerial_view_lut: CachedComputePipelineId, + pub render_sky: CachedRenderPipelineId, +} + +impl FromWorld for AtmospherePipelines { + fn from_world(world: &mut World) -> Self { + let pipeline_cache = world.resource::(); + let layouts = world.resource::(); + + let transmittance_lut = pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor { + label: Some("transmittance_lut_pipeline".into()), + layout: vec![layouts.transmittance_lut.clone()], + push_constant_ranges: vec![], + vertex: fullscreen_shader_vertex_state(), + primitive: PrimitiveState::default(), + depth_stencil: None, + multisample: MultisampleState::default(), + zero_initialize_workgroup_memory: false, + fragment: Some(FragmentState { + shader: shaders::TRANSMITTANCE_LUT.clone(), + shader_defs: vec![], + entry_point: "main".into(), + targets: vec![Some(ColorTargetState { + format: TextureFormat::Rgba16Float, + blend: None, + write_mask: ColorWrites::ALL, + })], + }), + }); + + let multiscattering_lut = + pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("multi_scattering_lut_pipeline".into()), + layout: vec![layouts.multiscattering_lut.clone()], + push_constant_ranges: vec![], + shader: shaders::MULTISCATTERING_LUT, + shader_defs: vec![], + entry_point: "main".into(), + zero_initialize_workgroup_memory: false, + }); + + let sky_view_lut = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("sky_view_lut_pipeline".into()), + layout: vec![layouts.sky_view_lut.clone()], + push_constant_ranges: vec![], + shader: shaders::SKY_VIEW_LUT, + shader_defs: vec![], + entry_point: "main".into(), + zero_initialize_workgroup_memory: false, + }); + + let aerial_view_lut = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("aerial_view_lut_pipeline".into()), + layout: vec![layouts.aerial_view_lut.clone()], + push_constant_ranges: vec![], + shader: shaders::AERIAL_VIEW_LUT, + shader_defs: vec![], + entry_point: "main".into(), + zero_initialize_workgroup_memory: false, + }); + + let render_sky = pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor { + label: Some("render_sky_pipeline".into()), + layout: vec![layouts.render_sky.clone()], + push_constant_ranges: vec![], + vertex: fullscreen_shader_vertex_state(), + primitive: PrimitiveState::default(), + depth_stencil: None, + multisample: MultisampleState::default(), + zero_initialize_workgroup_memory: false, + fragment: Some(FragmentState { + shader: shaders::RENDER_SKY.clone(), + shader_defs: vec![], + entry_point: "main".into(), + targets: vec![Some(ColorTargetState { + format: TextureFormat::Rgba16Float, + blend: Some(BlendState { + color: BlendComponent { + src_factor: BlendFactor::One, + dst_factor: BlendFactor::SrcAlpha, + operation: BlendOperation::Add, + }, + alpha: BlendComponent { + src_factor: BlendFactor::Zero, + dst_factor: BlendFactor::One, + operation: BlendOperation::Add, + }, + }), + write_mask: ColorWrites::ALL, + })], + }), + }); + + Self { + transmittance_lut, + multiscattering_lut, + sky_view_lut, + aerial_view_lut, + render_sky, + } + } +} + +#[derive(Component)] +pub struct AtmosphereTextures { + pub transmittance_lut: CachedTexture, + pub multiscattering_lut: CachedTexture, + pub sky_view_lut: CachedTexture, + pub sky_view_lut_cube_view: TextureView, + pub aerial_view_lut: CachedTexture, +} + +pub(super) fn prepare_atmosphere_textures( + views: Query<(Entity, &AtmosphereSettings), With>, + render_device: Res, + mut texture_cache: ResMut, + mut commands: Commands, +) { + for (entity, lut_settings) in &views { + let transmittance_lut = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("transmittance_lut"), + size: Extent3d { + width: lut_settings.transmittance_lut_size.x, + height: lut_settings.multiscattering_lut_size.y, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba16Float, + usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ); + + let multiscattering_lut = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("multiscattering_lut"), + size: Extent3d { + width: lut_settings.multiscattering_lut_size.x, + height: lut_settings.multiscattering_lut_size.y, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba16Float, + usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ); + + let sky_view_lut = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("sky_view_lut"), + size: Extent3d { + width: lut_settings.sky_view_lut_size, + height: lut_settings.sky_view_lut_size, + depth_or_array_layers: 6, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba16Float, + usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ); + + let sky_view_lut_cube_view = sky_view_lut.texture.create_view(&TextureViewDescriptor { + label: Some("sky_view_lut_cube"), + dimension: Some(TextureViewDimension::Cube), + ..Default::default() + }); + + let aerial_view_lut = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("aerial_view_lut"), + size: Extent3d { + width: lut_settings.aerial_view_lut_size.x, + height: lut_settings.aerial_view_lut_size.y, + depth_or_array_layers: lut_settings.aerial_view_lut_size.z, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D3, + format: TextureFormat::Rgba16Float, + usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ); + + commands.entity(entity).insert({ + AtmosphereTextures { + transmittance_lut, + multiscattering_lut, + sky_view_lut, + sky_view_lut_cube_view, + aerial_view_lut, + } + }); + } +} + +#[derive(Component)] +pub(crate) struct AtmosphereBindGroups { + pub transmittance_lut: BindGroup, + pub multiscattering_lut: BindGroup, + pub sky_view_lut: BindGroup, + pub aerial_view_lut: BindGroup, + pub render_sky: BindGroup, +} + +#[expect(clippy::too_many_arguments)] +pub(super) fn prepare_atmosphere_bind_groups( + views: Query< + (Entity, &AtmosphereTextures, &ViewDepthTexture), + (With, With), + >, + render_device: Res, + layouts: Res, + samplers: Res, + view_uniforms: Res, + lights_uniforms: Res, + atmosphere_uniforms: Res>, + settings_uniforms: Res>, + + mut commands: Commands, +) { + let atmosphere_binding = atmosphere_uniforms + .binding() + .expect("Failed to prepare atmosphere bind groups. Atmosphere uniform buffer missing"); + + let settings_binding = settings_uniforms.binding().expect( + "Failed to prepare atmosphere bind groups. AtmosphereSettings uniform buffer missing", + ); + + let view_binding = view_uniforms + .uniforms + .binding() + .expect("Failed to prepare atmosphere bind groups. View uniform buffer missing"); + + let lights_binding = lights_uniforms + .view_gpu_lights + .binding() + .expect("Failed to prepare atmosphere bind groups. Lights uniform buffer missing"); + + for (entity, textures, view_depth_texture) in &views { + let transmittance_lut = render_device.create_bind_group( + "transmittance_lut_bind_group", + &layouts.transmittance_lut, + &BindGroupEntries::with_indices(( + (0, atmosphere_binding.clone()), + (1, settings_binding.clone()), + )), + ); + + let multiscattering_lut = render_device.create_bind_group( + "multiscattering_lut_bind_group", + &layouts.multiscattering_lut, + &BindGroupEntries::with_indices(( + (0, atmosphere_binding.clone()), + (1, settings_binding.clone()), + (4, &textures.transmittance_lut.default_view), + (5, &samplers.transmittance_lut), + (12, &textures.multiscattering_lut.default_view), + )), + ); + + let sky_view_lut = render_device.create_bind_group( + "sky_view_lut_bind_group", + &layouts.sky_view_lut, + &BindGroupEntries::with_indices(( + (0, atmosphere_binding.clone()), + (1, settings_binding.clone()), + (2, view_binding.clone()), + (3, lights_binding.clone()), + (4, &textures.transmittance_lut.default_view), + (5, &samplers.transmittance_lut), + (6, &textures.multiscattering_lut.default_view), + (7, &samplers.multiscattering_lut), + (12, &textures.sky_view_lut.default_view), + )), + ); + + let aerial_view_lut = render_device.create_bind_group( + "sky_view_lut_bind_group", + &layouts.aerial_view_lut, + &BindGroupEntries::with_indices(( + (0, atmosphere_binding.clone()), + (1, settings_binding.clone()), + (2, view_binding.clone()), + (3, lights_binding.clone()), + (4, &textures.transmittance_lut.default_view), + (5, &samplers.transmittance_lut), + (6, &textures.multiscattering_lut.default_view), + (7, &samplers.multiscattering_lut), + (12, &textures.aerial_view_lut.default_view), + )), + ); + + let render_sky = render_device.create_bind_group( + "render_sky_bind_group", + &layouts.render_sky, + &BindGroupEntries::with_indices(( + (0, atmosphere_binding.clone()), + (1, settings_binding.clone()), + (2, view_binding.clone()), + (3, lights_binding.clone()), + (4, &textures.transmittance_lut.default_view), + (5, &samplers.transmittance_lut), + (8, &textures.sky_view_lut_cube_view), + (9, &samplers.sky_view_lut), + (10, &textures.aerial_view_lut.default_view), + (11, &samplers.aerial_view_lut), + (12, view_depth_texture.view()), + )), + ); + + commands.entity(entity).insert(AtmosphereBindGroups { + transmittance_lut, + multiscattering_lut, + sky_view_lut, + aerial_view_lut, + render_sky, + }); + } +} diff --git a/crates/bevy_pbr/src/atmosphere/sky_view_lut.wgsl b/crates/bevy_pbr/src/atmosphere/sky_view_lut.wgsl new file mode 100644 index 0000000000000..dbc54f9c5eaa9 --- /dev/null +++ b/crates/bevy_pbr/src/atmosphere/sky_view_lut.wgsl @@ -0,0 +1,85 @@ +#import bevy_pbr::{ + mesh_view_types::Lights, + atmosphere::{ + types::{Atmosphere, AtmosphereSettings}, + bindings::{atmosphere, view, settings}, + functions::{ + sample_atmosphere, get_local_up, AtmosphereSample, + sample_local_inscattering, get_local_r, view_radius, + sky_view_lut_unsquash_ray_dir, direction_view_to_world + }, + bruneton_functions::distance_to_top_atmosphere_boundary, + } +} + +#import bevy_render::view::View; +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput + +@group(0) @binding(12) var sky_view_lut_out: texture_storage_2d_array; + +@compute +@workgroup_size(16, 16, 1) +fn main(@builtin(global_invocation_id) idx: vec3) { + let uv = (vec2(idx.xy) + vec2(0.5)) / f32(settings.sky_view_lut_size); + let ray_dir_vs_squashed = cubemap_coords_to_ray_dir(uv, idx.z); + let ray_dir_vs = sky_view_lut_unsquash_ray_dir(ray_dir_vs_squashed); + let ray_dir = direction_view_to_world(ray_dir_vs); + + let r = view_radius(); //TODO: paper says to center the sky view on the planet ground + let mu = ray_dir.y; + + let t_top = distance_to_top_atmosphere_boundary(r, mu); + let step_length = t_top / f32(settings.sky_view_lut_samples); + + var total_inscattering = vec3(0.0); + var optical_depth = vec3(0.0); + for (var step_i: u32 = 0u; step_i < settings.sky_view_lut_samples; step_i++) { + let t_i = step_length * (f32(step_i) + 0.5); //todo: 0.3???; + let local_r = get_local_r(r, mu, t_i); + let local_up = get_local_up(r, t_i, ray_dir); + + let local_atmosphere = sample_atmosphere(local_r); + optical_depth += local_atmosphere.extinction * step_length; + let transmittance_to_sample = exp(-optical_depth); + + var local_inscattering = sample_local_inscattering(local_atmosphere, transmittance_to_sample, ray_dir, local_r, local_up); + total_inscattering += local_inscattering * step_length; + } + + textureStore(sky_view_lut_out, idx.xy, idx.z, vec4(total_inscattering, 1.0)); +} + +fn cubemap_coords_to_ray_dir(uv: vec2, face_index: u32) -> vec3 { + let quotient: u32 = face_index / 2u; + let remainder: u32 = face_index % 2u; + let sign: f32 = 1.0 - 2.0 * f32(remainder); + var ray_dir = vec3(0.0); + let uv1_1 = uv * 2 - 1; + switch quotient { + case 0u: { // x axis + ray_dir = vec3(sign, -uv1_1.y, -sign * uv1_1.x); + } + case 1u: { // y axis + ray_dir = vec3(uv1_1.x, sign, sign * uv1_1.y); + } + case 2u: { // z axis + ray_dir = vec3(sign * uv1_1.x, -uv1_1.y, sign); + } + default: { + ray_dir = vec3(0.0, 1.0, 0.0); + } + } + return normalize(ray_dir); +} + + + +//lat-long projection [-pi, pi] x [-pi/2, pi/2] -> S^2 +fn get_ray_direction(lat_long: vec2) -> vec3 { + let cos_long = cos(lat_long.y); + let sin_long = sin(lat_long.y); + let horizontal_rotation = mat2x2(cos_long, -sin_long, sin_long, cos_long); + let horizontal = horizontal_rotation * vec2(-view.world_from_view[2].xz); + + return normalize(vec3(horizontal.x, sin(lat_long.x), horizontal.y)); +} diff --git a/crates/bevy_pbr/src/atmosphere/transmittance_lut.wgsl b/crates/bevy_pbr/src/atmosphere/transmittance_lut.wgsl new file mode 100644 index 0000000000000..cfabcbcda4a95 --- /dev/null +++ b/crates/bevy_pbr/src/atmosphere/transmittance_lut.wgsl @@ -0,0 +1,51 @@ +#import bevy_pbr::atmosphere::{ + types::{Atmosphere, AtmosphereSettings}, + bindings::{settings, atmosphere}, + functions::{AtmosphereSample, sample_atmosphere, get_local_r}, + bruneton_functions::{transmittance_lut_uv_to_r_mu, distance_to_bottom_atmosphere_boundary, distance_to_top_atmosphere_boundary}, +} + + +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput +@fragment +fn main(in: FullscreenVertexOutput) -> @location(0) vec4 { + // map UV coordinates to view height (r) and zenith cos angle (mu) + let r_mu = transmittance_lut_uv_to_r_mu(in.uv); + + // compute the optical depth from view height r to the top atmosphere boundary + let optical_depth = compute_optical_depth_to_top_atmosphere_boundary(r_mu.x, r_mu.y, settings.transmittance_lut_samples); + + let transmittance = exp(-optical_depth); + + return vec4(transmittance, 1.0); +} + +/// Compute the optical depth of the atmosphere from the ground to the top atmosphere boundary +/// at a given view height (r) and zenith cos angle (mu) +fn compute_optical_depth_to_top_atmosphere_boundary(r: f32, mu: f32, sample_count: u32) -> vec3 { + let t_bottom = distance_to_bottom_atmosphere_boundary(r, mu); + let t_top = distance_to_top_atmosphere_boundary(r, mu); + let t_max = max(t_bottom, t_top); //TODO: max? why not min? + + var optical_depth = vec3(0.0f); + var prev_t = 0.0f; + + for (var i = 0u; i < sample_count; i++) { + // SebH uses this for multiple scattering. It might not be needed here, but I've kept it to get results that are as close as possible to the original + + //TODO: check specific integration approach. + let t_i = (t_max * f32(i) + 0.3f) / f32(sample_count); //TODO: should be 0.5f? + let dt = t_i - prev_t; + prev_t = t_i; + + // distance r from current sample point to planet center + let r_i = get_local_r(r, mu, t_i); + + let atmosphere_sample = sample_atmosphere(r_i); + let sample_optical_depth = atmosphere_sample.extinction * dt; + + optical_depth += sample_optical_depth; + } + + return optical_depth; +} diff --git a/crates/bevy_pbr/src/atmosphere/types.wgsl b/crates/bevy_pbr/src/atmosphere/types.wgsl new file mode 100644 index 0000000000000..b28635fa2f34a --- /dev/null +++ b/crates/bevy_pbr/src/atmosphere/types.wgsl @@ -0,0 +1,36 @@ +#define_import_path bevy_pbr::atmosphere::types + +struct Atmosphere { + // Radius of the planet + bottom_radius: f32, //units: km + + // Radius at which we consider the atmosphere to 'end' for out calculations (from center of planet) + top_radius: f32, //units: km + + ground_albedo: vec3, + + rayleigh_density_exp_scale: f32, + rayleigh_scattering: vec3, + + mie_density_exp_scale: f32, + mie_scattering: f32, //units: km^-1 + mie_absorption: f32, //units: km^-1 + mie_asymmetry: f32, //the "asymmetry" value of the phase function, unitless. Domain: (-1, 1) + + ozone_layer_center_altitude: f32, //units: km + ozone_layer_half_width: f32, //units: km + ozone_absorption: vec3, //ozone absorption. units: km^-1 +} + +struct AtmosphereSettings { + transmittance_lut_size: vec2, + multiscattering_lut_size: vec2, + aerial_view_lut_size: vec3, //Gross ordering for padding reasons + sky_view_lut_size: u32, + multiscattering_lut_dirs: u32, + transmittance_lut_samples: u32, + multiscattering_lut_samples: u32, + sky_view_lut_samples: u32, + aerial_view_lut_samples: u32, + scene_units_to_km: f32, +} diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 7526f384e2fe5..78cf2f83897f3 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -25,6 +25,7 @@ pub mod experimental { } } +mod atmosphere; mod bundle; mod cluster; pub mod deferred; @@ -46,6 +47,7 @@ mod volumetric_fog; use bevy_color::{Color, LinearRgba}; use core::marker::PhantomData; +pub use atmosphere::*; pub use bundle::*; pub use cluster::*; pub use extended_material::*; @@ -111,7 +113,7 @@ pub mod graph { } } -use crate::{deferred::DeferredPbrLightingPlugin, graph::NodePbr}; +use crate::{atmosphere::AtmospherePlugin, deferred::DeferredPbrLightingPlugin, graph::NodePbr}; use bevy_app::prelude::*; use bevy_asset::{load_internal_asset, AssetApp, Assets, Handle}; use bevy_core_pipeline::core_3d::graph::{Core3d, Node3d}; @@ -353,6 +355,7 @@ impl Plugin for PbrPlugin { SyncComponentPlugin::::default(), SyncComponentPlugin::::default(), )) + .add_plugins(AtmospherePlugin) .configure_sets( PostUpdate, ( diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.rs b/crates/bevy_pbr/src/render/mesh_view_bindings.rs index bbf158564d43b..e015b3a6fdade 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.rs @@ -45,7 +45,9 @@ use crate::{ self, IrradianceVolume, RenderViewIrradianceVolumeBindGroupEntries, IRRADIANCE_VOLUMES_ARE_USABLE, }, - prepass, EnvironmentMapUniformBuffer, FogMeta, GlobalClusterableObjectMeta, + prepass, + resources::{AtmosphereSamplers, AtmosphereTextures}, + Atmosphere, EnvironmentMapUniformBuffer, FogMeta, GlobalClusterableObjectMeta, GpuClusterableObjects, GpuFog, GpuLights, LightMeta, LightProbesBuffer, LightProbesUniform, MeshPipeline, MeshPipelineKey, RenderViewLightProbes, ScreenSpaceAmbientOcclusionResources, ScreenSpaceReflectionsBuffer, ScreenSpaceReflectionsUniform, ShadowSamplers, diff --git a/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs b/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs index 45f4c26c6cd22..abb43c80a7dc0 100644 --- a/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs +++ b/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs @@ -556,4 +556,16 @@ pub mod binding_types { } .into_bind_group_layout_entry_builder() } + + pub fn texture_storage_3d( + format: TextureFormat, + access: StorageTextureAccess, + ) -> BindGroupLayoutEntryBuilder { + BindingType::StorageTexture { + access, + format, + view_dimension: TextureViewDimension::D3, + } + .into_bind_group_layout_entry_builder() + } } diff --git a/docs/cargo_features.md b/docs/cargo_features.md index bb9d8d47aeff9..e569715183456 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -11,7 +11,6 @@ The default feature set enables most of the expected features of a game engine, |feature name|description| |-|-| -|android-game-activity|Android GameActivity support. Default, choose between this and `android-native-activity`.| |android_shared_stdcxx|Enable using a shared stdlib for cxx on Android| |animation|Enable animation support, and glTF animation loading| |bevy_animation|Provides animation functionality| @@ -22,20 +21,15 @@ The default feature set enables most of the expected features of a game engine, |bevy_gilrs|Adds gamepad support| |bevy_gizmos|Adds support for rendering gizmos| |bevy_gltf|[glTF](https://www.khronos.org/gltf/) support| -|bevy_mesh_picking_backend|Provides an implementation for picking meshes| |bevy_pbr|Adds PBR rendering| |bevy_picking|Provides picking functionality| -|bevy_remote|Enable the Bevy Remote Protocol| |bevy_render|Provides rendering functionality| |bevy_scene|Provides scene functionality| |bevy_sprite|Provides sprite functionality| -|bevy_sprite_picking_backend|Provides an implementation for picking sprites| |bevy_state|Enable built in global state machines| |bevy_text|Provides text functionality| |bevy_ui|A custom ECS-driven UI framework| -|bevy_ui_picking_backend|Provides an implementation for picking ui| |bevy_winit|winit window and input backend| -|custom_cursor|Enable winit custom cursor support| |default_font|Include a default font, containing only ASCII characters, at the cost of a 20kB binary size increase| |hdr|HDR image format support| |ktx2|KTX2 compressed texture support| @@ -54,6 +48,7 @@ The default feature set enables most of the expected features of a game engine, |feature name|description| |-|-| |accesskit_unix|Enable AccessKit on Unix backends (currently only works with experimental screen readers and forks.)| +|android-game-activity|Android GameActivity support. Default, choose between this and `android-native-activity`.| |android-native-activity|Android NativeActivity support. Legacy, should be avoided for most new Android games.| |asset_processor|Enables the built-in asset processor for processed assets.| |async-io|Use async-io's implementation of block_on instead of futures-lite's implementation. This is preferred if your application uses async-io.| @@ -61,7 +56,12 @@ The default feature set enables most of the expected features of a game engine, |bevy_ci_testing|Enable systems that allow for automated testing on CI| |bevy_debug_stepping|Enable stepping-based debugging of Bevy systems| |bevy_dev_tools|Provides a collection of developer tools| +|bevy_mesh_picking_backend|Provides an implementation for picking meshes| +|bevy_remote|Enable the Bevy Remote Protocol| +|bevy_sprite_picking_backend|Provides an implementation for picking sprites| +|bevy_ui_picking_backend|Provides an implementation for picking ui| |bmp|BMP image format support| +|custom_cursor|Enable winit custom cursor support| |dds|DDS compressed texture support| |debug_glam_assert|Enable assertions in debug builds to check the validity of parameters passed to glam| |detailed_trace|Enable detailed trace event logging. These trace events are expensive even when off, thus they require compile time opt-in| diff --git a/examples/3d/atmosphere.rs b/examples/3d/atmosphere.rs new file mode 100644 index 0000000000000..c11495dbf959d --- /dev/null +++ b/examples/3d/atmosphere.rs @@ -0,0 +1,119 @@ +//! This example showcases pbr atmospheric scattering +//! +//! ## Controls +//! +//! | Key Binding | Action | +//! |:-------------------|:---------------------------------------| +//! | `Spacebar` | Toggle Atmospheric Fog | +//! | `S` | Toggle Directional Light Fog Influence | + +use std::f32::consts::PI; + +use bevy::{ + core_pipeline::core_3d::Camera3dDepthTextureUsage, + pbr::{Atmosphere, AtmosphereSettings, CascadeShadowConfigBuilder}, + prelude::*, +}; +use bevy_internal::core_pipeline::tonemapping::Tonemapping; +use bevy_render::render_resource::TextureUsages; +use light_consts::lux; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems( + Startup, + (setup_camera_fog, setup_terrain_scene, setup_instructions), + ) + .add_systems(Update, rotate_sun) + //.add_systems(Update, rotate_camera) + .run(); +} + +fn setup_camera_fog(mut commands: Commands) { + commands.spawn(( + Camera3d { + depth_texture_usages: Camera3dDepthTextureUsage::from( + TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, + ), + ..Default::default() + }, + Camera { + hdr: true, + ..default() + }, + Msaa::Off, + Tonemapping::AcesFitted, + Transform::from_xyz(-1.2, 0.15, 0.0).looking_at(Vec3::Y * 0.15, Vec3::Y), + Atmosphere::EARTH, + AtmosphereSettings { + scene_units_to_km: 1.0, + ..Default::default() + }, + )); +} + +fn setup_terrain_scene(mut commands: Commands, asset_server: Res) { + // Configure a properly scaled cascade shadow map for this scene (defaults are too large, mesh units are in km) + let cascade_shadow_config = CascadeShadowConfigBuilder { + first_cascade_far_bound: 0.3, + maximum_distance: 3.0, + ..default() + } + .build(); + + // Sun + commands.spawn(( + DirectionalLight { + color: Color::srgb(0.98, 0.95, 0.82), + shadows_enabled: true, + illuminance: lux::AMBIENT_DAYLIGHT, + ..default() + }, + Transform::from_xyz(1.0, -1.0, 0.0).looking_at(Vec3::ZERO, Vec3::Y), + cascade_shadow_config, + )); + + // Terrain + commands.spawn(SceneRoot(asset_server.load( + GltfAssetLabel::Scene(0).from_asset("models/terrain/Mountains.gltf"), + ))); +} + +//TODO: update this +fn setup_instructions(mut commands: Commands) { + // commands.spawn( + // TextBundle::from_section( + // "Press Spacebar to Toggle Atmospheric Fog.\nPress S to Toggle Directional Light Fog Influence.", + // TextStyle::default(), + // ) + // .with_style(Style { + // position_type: PositionType::Absolute, + // bottom: Val::Px(12.0), + // left: Val::Px(12.0), + // ..default() + // }), + // ); +} + +// fn toggle_system(keycode: Res>, mut fog: Query<&mut FogSettings>) { +// let mut fog_settings = fog.single_mut(); +// +// if keycode.just_pressed(KeyCode::Space) { +// let a = fog_settings.color.alpha(); +// fog_settings.color.set_alpha(1.0 - a); +// } +// +// if keycode.just_pressed(KeyCode::KeyS) { +// let a = fog_settings.directional_light_color.alpha(); +// fog_settings.directional_light_color.set_alpha(0.5 - a); +// } +// } + +fn rotate_sun(mut sun: Single<&mut Transform, With>, time: Res