diff --git a/assets/environment_maps/info.txt b/assets/environment_maps/info.txt index f13321817b..513ffbd5f1 100644 --- a/assets/environment_maps/info.txt +++ b/assets/environment_maps/info.txt @@ -1,6 +1,7 @@ -The pisa_*.ktx2 files were generated from https://github.com/KhronosGroup/glTF-Sample-Environments/blob/master/pisa.hdr using the following tools and commands: -- IBL environment map prefiltering to cubemaps: https://github.com/KhronosGroup/glTF-IBL-Sampler - - Diffuse: ./cli -inputPath pisa.hdr -outCubeMap pisa_diffuse.ktx2 -distribution Lambertian -cubeMapResolution 32 - - Specular: ./cli -inputPath pisa.hdr -outCubeMap pisa_specular.ktx2 -distribution GGX -cubeMapResolution 512 +The the spiaggia_di_mondello_*.ktx2 files were downloaded from https://polyhaven.com/a/spiaggia_di_mondello distributed under the CC0 license by Andreas Mischok. +- IBL environment map prefiltering to cubemaps: https://github.com/mate-h/blender-envmap + - Imported the GLTF scene into Blender and saved the scene into the blend file + - Exported the reflection probe and the environment map as KTX2 files using the command line tool + - blender-envmap spiaggia_di_mondello_2k.exr --resolution 512 - Converting to rgb9e5 format with zstd 'supercompression': https://github.com/DGriffin91/bevy_mod_environment_map_tools - - cargo run --release -- --inputs pisa_diffuse.ktx2,pisa_specular.ktx2 --outputs pisa_diffuse_rgb9e5_zstd.ktx2,pisa_specular_rgb9e5_zstd.ktx2 + - cargo run --release -- --inputs spiaggia_di_mondello_*.ktx2 --outputs spiaggia_di_mondello_*_rgb9e5_zstd.ktx2 diff --git a/assets/environment_maps/spiaggia_di_mondello_2k_diffuse_rgb5e9.ktx2 b/assets/environment_maps/spiaggia_di_mondello_diffuse.ktx2 similarity index 100% rename from assets/environment_maps/spiaggia_di_mondello_2k_diffuse_rgb5e9.ktx2 rename to assets/environment_maps/spiaggia_di_mondello_diffuse.ktx2 diff --git a/assets/environment_maps/spiaggia_di_mondello_environment_map.ktx2 b/assets/environment_maps/spiaggia_di_mondello_environment_map.ktx2 new file mode 100644 index 0000000000..18e0caaaef Binary files /dev/null and b/assets/environment_maps/spiaggia_di_mondello_environment_map.ktx2 differ diff --git a/assets/environment_maps/spiaggia_di_mondello_2k_probe_diffuse_rgb5e9.ktx2 b/assets/environment_maps/spiaggia_di_mondello_probe_diffuse.ktx2 similarity index 100% rename from assets/environment_maps/spiaggia_di_mondello_2k_probe_diffuse_rgb5e9.ktx2 rename to assets/environment_maps/spiaggia_di_mondello_probe_diffuse.ktx2 diff --git a/assets/environment_maps/spiaggia_di_mondello_2k_probe_specular_rgb5e9.ktx2 b/assets/environment_maps/spiaggia_di_mondello_probe_specular.ktx2 similarity index 100% rename from assets/environment_maps/spiaggia_di_mondello_2k_probe_specular_rgb5e9.ktx2 rename to assets/environment_maps/spiaggia_di_mondello_probe_specular.ktx2 diff --git a/assets/environment_maps/spiaggia_di_mondello_2k_specular_rgb5e9.ktx2 b/assets/environment_maps/spiaggia_di_mondello_specular.ktx2 similarity index 100% rename from assets/environment_maps/spiaggia_di_mondello_2k_specular_rgb5e9.ktx2 rename to assets/environment_maps/spiaggia_di_mondello_specular.ktx2 diff --git a/crates/bevy_pbr/src/light_probe/environment_filter.wgsl b/crates/bevy_pbr/src/light_probe/environment_filter.wgsl index 9d1b14a2df..dade0cf9dd 100644 --- a/crates/bevy_pbr/src/light_probe/environment_filter.wgsl +++ b/crates/bevy_pbr/src/light_probe/environment_filter.wgsl @@ -1,5 +1,5 @@ #import bevy_render::maths::{PI, PI_2}; -#import bevy_pbr::lighting::perceptualRoughnessToRoughness; +#import bevy_pbr::utils::{build_orthonormal_basis}; struct FilteringConstants { mip_level: f32, @@ -92,18 +92,6 @@ fn sample_environment(dir: vec3f, level: f32) -> vec4f { return textureSampleLevel(input_texture, input_sampler, cube_uv.uv, cube_uv.face, level); } -// Calculate tangent space for the given normal -fn calculate_tangent_frame(normal: vec3f) -> mat3x3f { - // Use a robust method to pick a tangent - var up = vec3f(1.0, 0.0, 0.0); - if abs(normal.z) < 0.999 { - up = vec3f(0.0, 0.0, 1.0); - } - let tangent = normalize(cross(up, normal)); - let bitangent = cross(normal, tangent); - return mat3x3f(tangent, bitangent, normal); -} - // Hammersley sequence for quasi-random points fn hammersley_2d(i: u32, n: u32) -> vec2f { // Van der Corput sequence @@ -125,7 +113,17 @@ fn sample_noise(pixel_coords: vec2u) -> vec4f { return textureSampleLevel(blue_noise_texture, input_sampler, uv, 0.0); } +// from bevy_pbr/src/render/pbr_lighting.wgsl +fn perceptualRoughnessToRoughness(perceptualRoughness: f32) -> f32 { + // clamp perceptual roughness to prevent precision problems + // According to Filament design 0.089 is recommended for mobile + // Filament uses 0.045 for non-mobile + let clampedPerceptualRoughness = clamp(perceptualRoughness, 0.089, 1.0); + return clampedPerceptualRoughness * clampedPerceptualRoughness; +} + // GGX/Trowbridge-Reitz normal distribution function (D term) +// from bevy_pbr/src/render/pbr_lighting.wgsl fn D_GGX(roughness: f32, NdotH: f32) -> f32 { let oneMinusNdotHSquared = 1.0 - NdotH * NdotH; let a = NdotH * roughness; @@ -136,14 +134,11 @@ fn D_GGX(roughness: f32, NdotH: f32) -> f32 { // Importance sample GGX normal distribution function for a given roughness fn importance_sample_ggx(xi: vec2f, roughness: f32, normal: vec3f) -> vec3f { - // Use roughness^2 to ensure correct specular highlights - let a = roughness * roughness; - // Sample in spherical coordinates let phi = PI_2 * xi.x; // GGX mapping from uniform random to GGX distribution - let cos_theta = sqrt((1.0 - xi.y) / (1.0 + (a * a - 1.0) * xi.y)); + let cos_theta = sqrt((1.0 - xi.y) / (1.0 + (roughness * roughness - 1.0) * xi.y)); let sin_theta = sqrt(1.0 - cos_theta * cos_theta); // Convert to cartesian @@ -154,7 +149,7 @@ fn importance_sample_ggx(xi: vec2f, roughness: f32, normal: vec3f) -> vec3f { ); // Transform from tangent to world space - return calculate_tangent_frame(normal) * h; + return build_orthonormal_basis(normal) * h; } // Calculate LOD for environment map lookup using filtered importance sampling @@ -171,7 +166,7 @@ fn calculate_environment_map_lod(pdf: f32, width: f32, samples: f32) -> f32 { // Smith geometric shadowing function fn G_Smith(NoV: f32, NoL: f32, roughness: f32) -> f32 { - let k = (roughness * roughness) / 2.0; + let k = roughness / 2.0; let GGXL = NoL / (NoL * (1.0 - k) + k); let GGXV = NoV / (NoV * (1.0 - k) + k); return GGXL * GGXV; @@ -197,8 +192,9 @@ fn generate_radiance_map(@builtin(global_invocation_id) global_id: vec3u) { // For radiance map, view direction = normal for perfect reflection let view = normal; - // Get the roughness parameter - let roughness = constants.roughness; + // Convert perceptual roughness to physical microfacet roughness + let perceptual_roughness = constants.roughness; + let roughness = perceptualRoughnessToRoughness(perceptual_roughness); // Get blue noise offset for stratification let vector_noise = sample_noise(coords); @@ -292,7 +288,7 @@ fn uniform_sample_sphere(i: u32, normal: vec3f) -> vec3f { z ); - let tangent_frame = calculate_tangent_frame(normal); + let tangent_frame = build_orthonormal_basis(normal); return normalize(tangent_frame * dir_uniform); } diff --git a/crates/bevy_pbr/src/light_probe/generate.rs b/crates/bevy_pbr/src/light_probe/generate.rs index b9d00a623d..269e893516 100644 --- a/crates/bevy_pbr/src/light_probe/generate.rs +++ b/crates/bevy_pbr/src/light_probe/generate.rs @@ -51,9 +51,8 @@ use bevy_render::{ use bevy_light::{EnvironmentMapLight, GeneratedEnvironmentMapLight}; -/// Sphere Cosine Weighted Irradiance shader handle -pub const STBN_SPHERE: Handle = uuid_handle!("3110b545-78e0-48fc-b86e-8bc0ea50fc67"); -pub const STBN_VEC2: Handle = uuid_handle!("3110b545-78e0-48fc-b86e-8bc0ea50fc67"); +/// Handle for Spatio-Temporal Blue Noise texture +pub const SBTN: Handle = uuid_handle!("3110b545-78e0-48fc-b86e-8bc0ea50fc67"); /// Labels for the environment map generation nodes #[derive(PartialEq, Eq, Debug, Copy, Clone, Hash, RenderLabel)] @@ -506,13 +505,8 @@ pub fn prepare_generator_bind_groups( render_images: Res>, mut commands: Commands, ) { - // Get blue noise texture - let sphere_cosine_weights = render_images - .get(&STBN_SPHERE) - .expect("Sphere cosine weights texture not loaded"); - let vector2_uniform = render_images - .get(&STBN_VEC2) + .get(&SBTN) .expect("Vector2 uniform texture not loaded"); for (entity, textures, env_map_light) in &light_probes { @@ -644,8 +638,8 @@ pub fn prepare_generator_bind_groups( sample_count: 1024, roughness: 1.0, blue_noise_size: Vec2::new( - sphere_cosine_weights.size.width as f32, - sphere_cosine_weights.size.height as f32, + vector2_uniform.size.width as f32, + vector2_uniform.size.height as f32, ), white_point: env_map_light.white_point, }; @@ -671,7 +665,7 @@ pub fn prepare_generator_bind_groups( (1, &samplers.linear), (2, &irradiance_map), (3, &irradiance_constants_buffer), - (4, &sphere_cosine_weights.texture_view), + (4, &vector2_uniform.texture_view), )), ); diff --git a/crates/bevy_pbr/src/light_probe/mod.rs b/crates/bevy_pbr/src/light_probe/mod.rs index e74a0d8d04..1d99e43a5d 100644 --- a/crates/bevy_pbr/src/light_probe/mod.rs +++ b/crates/bevy_pbr/src/light_probe/mod.rs @@ -35,18 +35,15 @@ use bevy_render::{ Extract, ExtractSchedule, Render, RenderApp, RenderSystems, }; use bevy_transform::{components::Transform, prelude::GlobalTransform}; -use generate::{ - extract_generator_entities, generate_environment_map_light, prepare_generator_bind_groups, - prepare_intermediate_textures, GeneratorPipelines, SpdNode, STBN_SPHERE, STBN_VEC2, -}; use tracing::error; use core::{hash::Hash, ops::Deref}; use crate::{ generate::{ - GeneratorBindGroupLayouts, GeneratorNode, GeneratorSamplers, IrradianceMapNode, - RadianceMapNode, + extract_generator_entities, generate_environment_map_light, prepare_generator_bind_groups, + prepare_intermediate_textures, GeneratorBindGroupLayouts, GeneratorNode, + GeneratorPipelines, GeneratorSamplers, IrradianceMapNode, RadianceMapNode, SpdNode, SBTN, }, light_probe::environment_map::EnvironmentMapIds, }; @@ -308,11 +305,8 @@ impl Plugin for LightProbePlugin { embedded_asset!(app, "spd.wgsl"); embedded_asset!(app, "copy_mip0.wgsl"); - load_internal_binary_asset!( - app, - STBN_SPHERE, - "noise/sphere_coshemi_gauss1_0.png", - |bytes, _: String| Image::from_buffer( + load_internal_binary_asset!(app, SBTN, "sbtn_vec2.png", |bytes, _: String| { + Image::from_buffer( bytes, ImageType::Extension("png"), CompressedImageFormats::NONE, @@ -320,22 +314,8 @@ impl Plugin for LightProbePlugin { ImageSampler::Default, RenderAssetUsages::RENDER_WORLD, ) - .expect("Failed to load sphere cosine weighted blue noise texture") - ); - load_internal_binary_asset!( - app, - STBN_VEC2, - "noise/vector2_uniform_gauss1_0.png", - |bytes, _: String| Image::from_buffer( - bytes, - ImageType::Extension("png"), - CompressedImageFormats::NONE, - false, - ImageSampler::Default, - RenderAssetUsages::RENDER_WORLD, - ) - .expect("Failed to load vector2 uniform blue noise texture") - ); + .expect("Failed to load spatio-temporal blue noise texture") + }); app.add_plugins(ExtractInstancesPlugin::::new()) .add_plugins(SyncComponentPlugin::::default()) diff --git a/crates/bevy_pbr/src/light_probe/noise/sphere_coshemi_gauss1_0.png b/crates/bevy_pbr/src/light_probe/noise/sphere_coshemi_gauss1_0.png deleted file mode 100644 index 08199965dc..0000000000 Binary files a/crates/bevy_pbr/src/light_probe/noise/sphere_coshemi_gauss1_0.png and /dev/null differ diff --git a/crates/bevy_pbr/src/light_probe/noise/vector2_uniform_gauss1_0.png b/crates/bevy_pbr/src/light_probe/noise/vector2_uniform_gauss1_0.png deleted file mode 100644 index c9b61dcae5..0000000000 Binary files a/crates/bevy_pbr/src/light_probe/noise/vector2_uniform_gauss1_0.png and /dev/null differ diff --git a/crates/bevy_pbr/src/light_probe/sbtn_vec2.png b/crates/bevy_pbr/src/light_probe/sbtn_vec2.png new file mode 100644 index 0000000000..429b69fde0 Binary files /dev/null and b/crates/bevy_pbr/src/light_probe/sbtn_vec2.png differ diff --git a/crates/bevy_pbr/src/render/utils.wgsl b/crates/bevy_pbr/src/render/utils.wgsl index c887e3005e..86c07def39 100644 --- a/crates/bevy_pbr/src/render/utils.wgsl +++ b/crates/bevy_pbr/src/render/utils.wgsl @@ -68,6 +68,16 @@ fn octahedral_decode_signed(v: vec2) -> vec3 { return normalize(n); } +// https://jcgt.org/published/0006/01/01/paper.pdf +fn build_orthonormal_basis(normal: vec3) -> mat3x3 { + let sign = select(-1.0, 1.0, normal.z >= 0.0); + let a = -1.0 / (sign + normal.z); + let b = normal.x * normal.y * a; + let tangent = vec3(1.0 + sign * normal.x * normal.x * a, sign * b, -sign * normal.x); + let bitangent = vec3(b, sign + normal.y * normal.y * a, -normal.y); + return mat3x3(tangent, bitangent, normal); +} + // https://blog.demofox.org/2022/01/01/interleaved-gradient-noise-a-different-kind-of-low-discrepancy-sequence fn interleaved_gradient_noise(pixel_coordinates: vec2, frame: u32) -> f32 { let xy = pixel_coordinates + 5.588238 * f32(frame % 64u); diff --git a/crates/bevy_solari/src/scene/sampling.wgsl b/crates/bevy_solari/src/scene/sampling.wgsl index be709f0bc8..53566126b1 100644 --- a/crates/bevy_solari/src/scene/sampling.wgsl +++ b/crates/bevy_solari/src/scene/sampling.wgsl @@ -1,6 +1,6 @@ #define_import_path bevy_solari::sampling -#import bevy_pbr::utils::{rand_f, rand_vec2f, rand_range_u} +#import bevy_pbr::utils::{rand_f, rand_vec2f, rand_range_u, build_orthonormal_basis} #import bevy_render::maths::{PI, PI_2} #import bevy_solari::scene_bindings::{trace_ray, RAY_T_MIN, RAY_T_MAX, light_sources, directional_lights, LIGHT_SOURCE_KIND_DIRECTIONAL, resolve_triangle_data_full} @@ -187,13 +187,3 @@ fn triangle_barycentrics(random: vec2) -> vec3 { if barycentrics.x + barycentrics.y > 1.0 { barycentrics = 1.0 - barycentrics; } return vec3(1.0 - barycentrics.x - barycentrics.y, barycentrics); } - -// https://jcgt.org/published/0006/01/01/paper.pdf -fn build_orthonormal_basis(normal: vec3) -> mat3x3 { - let sign = select(-1.0, 1.0, normal.z >= 0.0); - let a = -1.0 / (sign + normal.z); - let b = normal.x * normal.y * a; - let tangent = vec3(1.0 + sign * normal.x * normal.x * a, sign * b, -sign * normal.x); - let bitangent = vec3(b, sign + normal.y * normal.y * a, -normal.y); - return mat3x3(tangent, bitangent, normal); -} diff --git a/examples/3d/reflection_probes.rs b/examples/3d/reflection_probes.rs index 9e6499365c..bf0022d09e 100644 --- a/examples/3d/reflection_probes.rs +++ b/examples/3d/reflection_probes.rs @@ -53,14 +53,17 @@ struct Cubemaps { // The blurry diffuse cubemap that reflects the world, but not the cubes. diffuse_environment_map: Handle, - // The specular cubemap that reflects the world, but not the cubes. + // The specular cubemap mip chain that reflects the world, but not the cubes. specular_environment_map: Handle, // The blurry diffuse cubemap that reflects both the world and the cubes. diffuse_reflection_probe: Handle, - // The specular cubemap that reflects both the world and the cubes. + // The specular cubemap mip chain that reflects both the world and the cubes. specular_reflection_probe: Handle, + + // Environment map with a single mip level + environment_map: Handle, } fn main() { @@ -164,39 +167,18 @@ fn spawn_reflection_probe(commands: &mut Commands, cubemaps: &Cubemaps) { // 2.0 because the sphere's radius is 1.0 and we want to fully enclose it. Transform::from_scale(Vec3::splat(2.0)), )); - // spawn directional light for the sun - commands.spawn(( - DirectionalLight { - illuminance: 10_000.0, - ..default() - }, - // Roughly match the position of the sun in the environment map - Transform::from_xyz(1.0, 0.5, 0.7).looking_at(Vec3::ZERO, Vec3::Y), - )); } fn spawn_generated_environment_map(commands: &mut Commands, cubemaps: &Cubemaps) { commands.spawn(( LightProbe, GeneratedEnvironmentMapLight { - // Reuse the specular map for the generated environment map, even - // though it already has mip levels. In reality you would use a - // cubemap texture without mip levels and generate the mips using - // this component by filtering the cubemap on the GPU. - environment_map: cubemaps.specular_environment_map.clone(), + environment_map: cubemaps.environment_map.clone(), intensity: 5000.0, ..default() }, Transform::from_scale(Vec3::splat(2.0)), )); - // spawn directional light - commands.spawn(( - DirectionalLight { - illuminance: 30_000.0, - ..default() - }, - Transform::from_xyz(1.0, 0.5, 0.7).looking_at(Vec3::ZERO, Vec3::Y), - )); } // Spawns the help text. @@ -226,10 +208,7 @@ fn add_environment_map_to_camera( .entity(camera_entity) .insert(create_camera_environment_map_light(&cubemaps)) .insert(Skybox { - // Reuse the specular map for the skybox since it's not too blurry. - // In reality you wouldn't do this--you'd use a real skybox texture--but - // reusing the textures like this saves space in the Bevy repository. - image: cubemaps.specular_environment_map.clone(), + image: cubemaps.environment_map.clone(), brightness: 5000.0, ..default() }); @@ -384,13 +363,15 @@ impl FromWorld for Cubemaps { fn from_world(world: &mut World) -> Self { Cubemaps { diffuse_environment_map: world - .load_asset("environment_maps/spiaggia_di_mondello_2k_probe_diffuse_rgb5e9.ktx2"), + .load_asset("environment_maps/spiaggia_di_mondello_probe_diffuse.ktx2"), specular_environment_map: world - .load_asset("environment_maps/spiaggia_di_mondello_2k_specular_rgb5e9.ktx2"), + .load_asset("environment_maps/spiaggia_di_mondello_specular.ktx2"), specular_reflection_probe: world - .load_asset("environment_maps/spiaggia_di_mondello_2k_probe_specular_rgb5e9.ktx2"), + .load_asset("environment_maps/spiaggia_di_mondello_probe_specular.ktx2"), diffuse_reflection_probe: world - .load_asset("environment_maps/spiaggia_di_mondello_2k_probe_diffuse_rgb5e9.ktx2"), + .load_asset("environment_maps/spiaggia_di_mondello_probe_diffuse.ktx2"), + environment_map: world + .load_asset("environment_maps/spiaggia_di_mondello_environment_map.ktx2"), } } }