Revert changes to assets, any power of two texture size support

This commit is contained in:
Máté Homolya 2025-07-13 09:47:47 -07:00
parent 50645ecb29
commit 44990abb81
No known key found for this signature in database
13 changed files with 199 additions and 131 deletions

View File

@ -1,7 +1,6 @@
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
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
- Converting to rgb9e5 format with zstd 'supercompression': https://github.com/DGriffin91/bevy_mod_environment_map_tools
- cargo run --release -- --inputs spiaggia_di_mondello_*.ktx2 --outputs spiaggia_di_mondello_*_rgb9e5_zstd.ktx2
- cargo run --release -- --inputs pisa_diffuse.ktx2,pisa_specular.ktx2 --outputs pisa_diffuse_rgb9e5_zstd.ktx2,pisa_specular_rgb9e5_zstd.ktx2

Binary file not shown.

Binary file not shown.

View File

@ -118,7 +118,7 @@ pub struct GeneratedEnvironmentMapLight {
pub environment_map: Handle<Image>,
/// Scale factor applied to the diffuse and specular light generated by this
/// component. Expressed in cd/m² (candela per square metre).
/// component. Expressed in cd/m² (candela per square meter).
pub intensity: f32,
/// World-space rotation applied to the cubemap.

View File

@ -50,6 +50,7 @@ use bevy_render::{
};
use bevy_light::{EnvironmentMapLight, GeneratedEnvironmentMapLight};
use core::cmp::min;
/// Handle for Spatio-Temporal Blue Noise texture
pub const SBTN: Handle<Image> = uuid_handle!("3110b545-78e0-48fc-b86e-8bc0ea50fc67");
@ -141,34 +142,34 @@ impl FromWorld for GeneratorBindGroupLayouts {
StorageTextureAccess::WriteOnly,
),
), // Output mip 8
// (
// 9,
// texture_storage_2d_array(
// TextureFormat::Rgba16Float,
// StorageTextureAccess::WriteOnly,
// ),
// ), // Output mip 9
// (
// 10,
// texture_storage_2d_array(
// TextureFormat::Rgba16Float,
// StorageTextureAccess::WriteOnly,
// ),
// ), // Output mip 10
// (
// 11,
// texture_storage_2d_array(
// TextureFormat::Rgba16Float,
// StorageTextureAccess::WriteOnly,
// ),
// ), // Output mip 11
// (
// 12,
// texture_storage_2d_array(
// TextureFormat::Rgba16Float,
// StorageTextureAccess::WriteOnly,
// ),
// ), // Output mip 12
(
9,
texture_storage_2d_array(
TextureFormat::Rgba16Float,
StorageTextureAccess::WriteOnly,
),
), // Output mip 9
(
10,
texture_storage_2d_array(
TextureFormat::Rgba16Float,
StorageTextureAccess::WriteOnly,
),
), // Output mip 10
(
11,
texture_storage_2d_array(
TextureFormat::Rgba16Float,
StorageTextureAccess::WriteOnly,
),
), // Output mip 11
(
12,
texture_storage_2d_array(
TextureFormat::Rgba16Float,
StorageTextureAccess::WriteOnly,
),
), // Output mip 12
(13, sampler(SamplerBindingType::Filtering)), // Linear sampler
(14, uniform_buffer::<SpdConstants>(false)), // Uniforms
),
@ -428,25 +429,35 @@ pub struct IntermediateTextures {
pub environment_map: CachedTexture,
}
/// Returns the total number of mip levels for the provided square texture size.
/// `size` must be a power of two greater than zero. For example, `size = 512` → `9`.
#[inline]
fn compute_mip_count(size: u32) -> u32 {
debug_assert!(size.is_power_of_two());
32 - size.leading_zeros()
}
/// Prepares textures needed for single pass downsampling
pub fn prepare_intermediate_textures(
light_probes: Query<Entity, With<RenderEnvironmentMap>>,
light_probes: Query<(Entity, &RenderEnvironmentMap)>,
render_device: Res<RenderDevice>,
mut texture_cache: ResMut<TextureCache>,
mut commands: Commands,
) {
for entity in &light_probes {
// Create environment map with 8 mip levels (512x512 -> 1x1)
for (entity, env_map_light) in &light_probes {
let base_size = env_map_light.environment_map.size.width;
let mip_level_count = compute_mip_count(base_size);
let environment_map = texture_cache.get(
&render_device,
TextureDescriptor {
label: Some("intermediate_environment_map"),
size: Extent3d {
width: 512,
height: 512,
width: base_size,
height: base_size,
depth_or_array_layers: 6, // Cubemap faces
},
mip_level_count: 9, // 512, 256, 128, 64, 32, 16, 8, 4, 2, 1
mip_level_count,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Rgba16Float,
@ -505,15 +516,22 @@ pub fn prepare_generator_bind_groups(
render_images: Res<RenderAssets<GpuImage>>,
mut commands: Commands,
) {
let vector2_uniform = render_images
.get(&SBTN)
.expect("Vector2 uniform texture not loaded");
let stbn_texture = render_images.get(&SBTN).expect("STBN texture not loaded");
let texture_size = Vec2::new(
stbn_texture.size.width as f32,
stbn_texture.size.height as f32,
);
for (entity, textures, env_map_light) in &light_probes {
// Create SPD bind group
// Determine mip chain based on input size
let base_size = env_map_light.environment_map.size.width;
let mip_count = compute_mip_count(base_size);
let last_mip = mip_count - 1;
// Create SPD constants
let spd_constants = SpdConstants {
mips: 8, // Number of mip levels
inverse_input_size: Vec2::new(1.0 / 512.0, 1.0 / 512.0), // 1.0 / input size
mips: mip_count - 1, // Number of mips we are generating (excluding mip 0)
inverse_input_size: Vec2::new(1.0 / base_size as f32, 1.0 / base_size as f32),
_padding: 0,
};
@ -533,78 +551,125 @@ pub fn prepare_generator_bind_groups(
"spd_bind_group",
&layouts.spd,
&BindGroupEntries::with_indices((
// Source mip0
(0, &input_env_map),
// Destination mips 1 12 (duplicate the last valid view if the chain is shorter)
(
1,
&create_storage_view(&textures.environment_map.texture, 1, &render_device),
&create_storage_view(
&textures.environment_map.texture,
min(1, last_mip),
&render_device,
),
),
(
2,
&create_storage_view(&textures.environment_map.texture, 2, &render_device),
&create_storage_view(
&textures.environment_map.texture,
min(2, last_mip),
&render_device,
),
),
(
3,
&create_storage_view(&textures.environment_map.texture, 3, &render_device),
&create_storage_view(
&textures.environment_map.texture,
min(3, last_mip),
&render_device,
),
),
(
4,
&create_storage_view(&textures.environment_map.texture, 4, &render_device),
&create_storage_view(
&textures.environment_map.texture,
min(4, last_mip),
&render_device,
),
),
(
5,
&create_storage_view(&textures.environment_map.texture, 5, &render_device),
&create_storage_view(
&textures.environment_map.texture,
min(5, last_mip),
&render_device,
),
),
(
6,
&create_storage_view(&textures.environment_map.texture, 6, &render_device),
&create_storage_view(
&textures.environment_map.texture,
min(6, last_mip),
&render_device,
),
),
(
7,
&create_storage_view(&textures.environment_map.texture, 7, &render_device),
&create_storage_view(
&textures.environment_map.texture,
min(7, last_mip),
&render_device,
),
),
(
8,
&create_storage_view(&textures.environment_map.texture, 8, &render_device),
&create_storage_view(
&textures.environment_map.texture,
min(8, last_mip),
&render_device,
),
),
(
9,
&create_storage_view(
&textures.environment_map.texture,
min(9, last_mip),
&render_device,
),
),
(
10,
&create_storage_view(
&textures.environment_map.texture,
min(10, last_mip),
&render_device,
),
),
(
11,
&create_storage_view(
&textures.environment_map.texture,
min(11, last_mip),
&render_device,
),
),
(
12,
&create_storage_view(
&textures.environment_map.texture,
min(12, last_mip),
&render_device,
),
),
// (
// 9,
// &create_storage_view(&textures.environment_map.texture, 9, &render_device),
// ),
// (
// 10,
// &create_storage_view(&textures.environment_map.texture, 10, &render_device),
// ),
// (
// 11,
// &create_storage_view(&textures.environment_map.texture, 11, &render_device),
// ),
// (
// 12,
// &create_storage_view(&textures.environment_map.texture, 12, &render_device),
// ),
(13, &samplers.linear),
(14, &spd_constants_buffer),
)),
);
// Create radiance map bind groups for each mip level
let num_mips = 9;
let num_mips = mip_count as usize;
let mut radiance_bind_groups = Vec::with_capacity(num_mips);
for mip in 0..num_mips {
// Calculate roughness from 0.0 (mip 0) to 0.889 (mip 8)
// We don't need roughness=1.0 as a mip level because it's handled by the separate diffuse irradiance map
let roughness = mip as f32 / num_mips as f32;
let roughness = mip as f32 / (num_mips - 1) as f32;
let sample_count = 32u32 * 2u32.pow((roughness * 4.0) as u32);
let radiance_constants = FilteringConstants {
mip_level: mip as f32,
sample_count,
roughness,
blue_noise_size: Vec2::new(
vector2_uniform.size.width as f32,
vector2_uniform.size.height as f32,
),
blue_noise_size: texture_size,
white_point: env_map_light.white_point,
};
@ -617,14 +682,14 @@ pub fn prepare_generator_bind_groups(
&render_device,
);
let bind_group = render_device.create_bind_group(
Some(format!("radiance_bind_group_mip_{}", mip).as_str()),
Some(format!("radiance_bind_group_mip_{mip}").as_str()),
&layouts.radiance,
&BindGroupEntries::with_indices((
(0, &textures.environment_map.default_view),
(1, &samplers.linear),
(2, &mip_storage_view),
(3, &radiance_constants_buffer),
(4, &vector2_uniform.texture_view),
(4, &stbn_texture.texture_view),
)),
);
@ -637,10 +702,7 @@ pub fn prepare_generator_bind_groups(
// 32 phi, 32 theta = 1024 samples total
sample_count: 1024,
roughness: 1.0,
blue_noise_size: Vec2::new(
vector2_uniform.size.width as f32,
vector2_uniform.size.height as f32,
),
blue_noise_size: texture_size,
white_point: env_map_light.white_point,
};
@ -665,7 +727,7 @@ pub fn prepare_generator_bind_groups(
(1, &samplers.linear),
(2, &irradiance_map),
(3, &irradiance_constants_buffer),
(4, &vector2_uniform.texture_view),
(4, &stbn_texture.texture_view),
)),
);
@ -698,7 +760,7 @@ pub fn prepare_generator_bind_groups(
/// Helper function to create a storage texture view for a specific mip level
fn create_storage_view(texture: &Texture, mip: u32, _render_device: &RenderDevice) -> TextureView {
texture.create_view(&TextureViewDescriptor {
label: Some(format!("storage_view_mip_{}", mip).as_str()),
label: Some(format!("storage_view_mip_{mip}").as_str()),
format: Some(texture.format()),
dimension: Some(TextureViewDimension::D2Array),
aspect: TextureAspect::All,
@ -790,11 +852,10 @@ impl Node for SpdNode {
compute_pass.set_pipeline(spd_first_pipeline);
compute_pass.set_bind_group(0, &bind_groups.spd, &[]);
// Calculate the optimal dispatch size based on our shader's workgroup size and thread mapping
// The workgroup size is 256x1x1, and our remap_for_wave_reduction maps these threads to a 8x8 block
// For a 512x512 texture, we need 512/64 = 8 workgroups in X and 512/64 = 8 workgroups in Y
// Each workgroup processes 64x64 pixels (256 threads each handling 16 pixels)
compute_pass.dispatch_workgroups(8, 8, 6); // 6 faces of cubemap
let tex_size = env_map_light.environment_map.size;
let wg_x = (tex_size.width / 64).max(1);
let wg_y = (tex_size.height / 64).max(1);
compute_pass.dispatch_workgroups(wg_x, wg_y, 6); // 6 faces
}
// Second pass - process mips 6-12
@ -810,8 +871,10 @@ impl Node for SpdNode {
compute_pass.set_pipeline(spd_second_pipeline);
compute_pass.set_bind_group(0, &bind_groups.spd, &[]);
// Dispatch workgroups - for each face
compute_pass.dispatch_workgroups(2, 2, 6);
let tex_size = env_map_light.environment_map.size;
let wg_x = (tex_size.width / 256).max(1);
let wg_y = (tex_size.height / 256).max(1);
compute_pass.dispatch_workgroups(wg_x, wg_y, 6);
}
}
@ -821,7 +884,11 @@ impl Node for SpdNode {
/// Radiance map node for generating specular environment maps
pub struct RadianceMapNode {
query: QueryState<(Entity, Read<GeneratorBindGroups>)>,
query: QueryState<(
Entity,
Read<GeneratorBindGroups>,
Read<RenderEnvironmentMap>,
)>,
}
impl FromWorld for RadianceMapNode {
@ -851,7 +918,7 @@ impl Node for RadianceMapNode {
return Ok(());
};
for (_, bind_groups) in self.query.iter_manual(world) {
for (_, bind_groups, env_map_light) in self.query.iter_manual(world) {
let mut compute_pass =
render_context
.command_encoder()
@ -862,12 +929,14 @@ impl Node for RadianceMapNode {
compute_pass.set_pipeline(radiance_pipeline);
let base_size = env_map_light.specular_map.size.width;
// Process each mip level
for (mip, bind_group) in bind_groups.radiance.iter().enumerate() {
compute_pass.set_bind_group(0, bind_group, &[]);
// Calculate dispatch size based on mip level
let mip_size = 512u32 >> mip;
let mip_size = base_size >> mip;
let workgroup_count = mip_size.max(8) / 8;
// Dispatch for all 6 faces
@ -938,6 +1007,27 @@ pub fn generate_environment_map_light(
query: Query<(Entity, &GeneratedEnvironmentMapLight), Without<EnvironmentMapLight>>,
) {
for (entity, filtered_env_map) in &query {
// Validate and fetch the source cubemap so we can size our targets correctly
let Some(src_image) = images.get(&filtered_env_map.environment_map) else {
// Texture not ready yet try again next frame
continue;
};
let base_size = src_image.texture_descriptor.size.width;
// Sanity checks square, power-of-two, ≤ 8192
if src_image.texture_descriptor.size.height != base_size
|| !base_size.is_power_of_two()
|| base_size > 8192
{
panic!(
"GeneratedEnvironmentMapLight source cubemap must be square power-of-two ≤ 8192, got {}×{}",
base_size, src_image.texture_descriptor.size.height
);
}
let mip_count = compute_mip_count(base_size);
// Create a placeholder for the irradiance map
let mut diffuse = Image::new_fill(
Extent3d {
@ -961,11 +1051,11 @@ pub fn generate_environment_map_light(
let diffuse_handle = images.add(diffuse);
// Create a placeholder for the specular map
// Create a placeholder for the specular map. It matches the input cubemap resolution.
let mut specular = Image::new_fill(
Extent3d {
width: 512,
height: 512,
width: base_size,
height: base_size,
depth_or_array_layers: 6,
},
TextureDimension::D2,
@ -977,7 +1067,7 @@ pub fn generate_environment_map_light(
// Set up for mipmaps
specular.texture_descriptor.usage =
TextureUsages::TEXTURE_BINDING | TextureUsages::STORAGE_BINDING;
specular.texture_descriptor.mip_level_count = 9;
specular.texture_descriptor.mip_level_count = mip_count;
// When setting mip_level_count, we need to allocate appropriate data size
// For GPU-generated mipmaps, we can set data to None since the GPU will generate the data
@ -985,7 +1075,7 @@ pub fn generate_environment_map_light(
specular.texture_view_descriptor = Some(TextureViewDescriptor {
dimension: Some(TextureViewDimension::Cube),
mip_level_count: Some(9),
mip_level_count: Some(mip_count),
..Default::default()
});

View File

@ -334,10 +334,10 @@ fn spd_store(pix: vec2u, value: vec4f, mip: u32, slice: u32) {
case 5u: { textureStore(mip_6, pix, slice, value); }
case 6u: { textureStore(mip_7, pix, slice, value); }
case 7u: { textureStore(mip_8, pix, slice, value); }
// case 8u: { textureStore(mip_9, pix, slice, value); }
// case 9u: { textureStore(mip_10, pix, slice, value); }
// case 10u: { textureStore(mip_11, pix, slice, value); }
// case 11u: { textureStore(mip_12, pix, slice, value); }
case 8u: { textureStore(mip_9, pix, slice, value); }
case 9u: { textureStore(mip_10, pix, slice, value); }
case 10u: { textureStore(mip_11, pix, slice, value); }
case 11u: { textureStore(mip_12, pix, slice, value); }
default: {}
}
}

View File

@ -12,7 +12,6 @@ use bevy::{
prelude::*,
render::{render_resource::TextureUsages, view::Hdr},
};
use bevy_render::camera::Exposure;
use std::{
f32::consts::PI,
@ -56,14 +55,8 @@ struct Cubemaps {
// 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 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() {
@ -109,15 +102,6 @@ fn spawn_scene(commands: &mut Commands, asset_server: &AssetServer) {
commands.spawn(SceneRoot(
asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/cubes/Cubes.glb")),
));
// 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 camera.
@ -125,7 +109,6 @@ fn spawn_camera(commands: &mut Commands) {
commands.spawn((
Camera3d::default(),
Hdr,
Exposure { ev100: 12.5 },
Tonemapping::AcesFitted,
Transform::from_xyz(-6.483, 0.325, 4.381).looking_at(Vec3::ZERO, Vec3::Y),
));
@ -159,7 +142,7 @@ fn spawn_reflection_probe(commands: &mut Commands, cubemaps: &Cubemaps) {
commands.spawn((
LightProbe,
EnvironmentMapLight {
diffuse_map: cubemaps.diffuse_reflection_probe.clone(),
diffuse_map: cubemaps.diffuse_environment_map.clone(),
specular_map: cubemaps.specular_reflection_probe.clone(),
intensity: 5000.0,
..default()
@ -173,7 +156,7 @@ fn spawn_generated_environment_map(commands: &mut Commands, cubemaps: &Cubemaps)
commands.spawn((
LightProbe,
GeneratedEnvironmentMapLight {
environment_map: cubemaps.environment_map.clone(),
environment_map: cubemaps.specular_environment_map.clone(),
intensity: 5000.0,
..default()
},
@ -208,7 +191,7 @@ fn add_environment_map_to_camera(
.entity(camera_entity)
.insert(create_camera_environment_map_light(&cubemaps))
.insert(Skybox {
image: cubemaps.environment_map.clone(),
image: cubemaps.specular_environment_map.clone(),
brightness: 5000.0,
..default()
});
@ -363,15 +346,11 @@ impl FromWorld for Cubemaps {
fn from_world(world: &mut World) -> Self {
Cubemaps {
diffuse_environment_map: world
.load_asset("environment_maps/spiaggia_di_mondello_probe_diffuse.ktx2"),
.load_asset("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
specular_environment_map: world
.load_asset("environment_maps/spiaggia_di_mondello_specular.ktx2"),
.load_asset("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
specular_reflection_probe: world
.load_asset("environment_maps/spiaggia_di_mondello_probe_specular.ktx2"),
diffuse_reflection_probe: world
.load_asset("environment_maps/spiaggia_di_mondello_probe_diffuse.ktx2"),
environment_map: world
.load_asset("environment_maps/spiaggia_di_mondello_environment_map.ktx2"),
.load_asset("environment_maps/cubes_reflection_probe_specular_rgb9e5_zstd.ktx2"),
}
}
}