Simplify and modularize code, update envmap info txt

This commit is contained in:
Máté Homolya 2025-07-12 00:36:54 -07:00
parent 178047b45b
commit 50645ecb29
No known key found for this signature in database
15 changed files with 61 additions and 109 deletions

View File

@ -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

View File

@ -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);
}

View File

@ -51,9 +51,8 @@ use bevy_render::{
use bevy_light::{EnvironmentMapLight, GeneratedEnvironmentMapLight};
/// Sphere Cosine Weighted Irradiance shader handle
pub const STBN_SPHERE: Handle<Image> = uuid_handle!("3110b545-78e0-48fc-b86e-8bc0ea50fc67");
pub const STBN_VEC2: Handle<Image> = uuid_handle!("3110b545-78e0-48fc-b86e-8bc0ea50fc67");
/// Handle for Spatio-Temporal Blue Noise texture
pub const SBTN: Handle<Image> = 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<RenderAssets<GpuImage>>,
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),
)),
);

View File

@ -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::<EnvironmentMapIds>::new())
.add_plugins(SyncComponentPlugin::<GeneratedEnvironmentMapLight>::default())

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -68,6 +68,16 @@ fn octahedral_decode_signed(v: vec2<f32>) -> vec3<f32> {
return normalize(n);
}
// https://jcgt.org/published/0006/01/01/paper.pdf
fn build_orthonormal_basis(normal: vec3<f32>) -> mat3x3<f32> {
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<f32>, frame: u32) -> f32 {
let xy = pixel_coordinates + 5.588238 * f32(frame % 64u);

View File

@ -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<f32>) -> vec3<f32> {
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<f32>) -> mat3x3<f32> {
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);
}

View File

@ -53,14 +53,17 @@ struct Cubemaps {
// The blurry diffuse cubemap that reflects the world, but not the cubes.
diffuse_environment_map: Handle<Image>,
// 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<Image>,
// The blurry diffuse cubemap that reflects both the world and the cubes.
diffuse_reflection_probe: Handle<Image>,
// 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<Image>,
// Environment map with a single mip level
environment_map: Handle<Image>,
}
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"),
}
}
}