diff --git a/crates/bevy_pbr/src/lightmap/lightmap.wgsl b/crates/bevy_pbr/src/lightmap/lightmap.wgsl index e58ec96870..da10ece9b1 100644 --- a/crates/bevy_pbr/src/lightmap/lightmap.wgsl +++ b/crates/bevy_pbr/src/lightmap/lightmap.wgsl @@ -13,33 +13,87 @@ // Samples the lightmap, if any, and returns indirect illumination from it. fn lightmap(uv: vec2, exposure: f32, instance_index: u32) -> vec3 { let packed_uv_rect = mesh[instance_index].lightmap_uv_rect; - let uv_rect = vec4(vec4( - packed_uv_rect.x & 0xffffu, - packed_uv_rect.x >> 16u, - packed_uv_rect.y & 0xffffu, - packed_uv_rect.y >> 16u)) / 65535.0; - + let uv_rect = vec4( + unpack2x16unorm(packed_uv_rect.x), + unpack2x16unorm(packed_uv_rect.y), + ); let lightmap_uv = mix(uv_rect.xy, uv_rect.zw, uv); + let lightmap_slot = mesh[instance_index].material_and_lightmap_bind_group_slot >> 16u; + // Bicubic 4-tap + // https://developer.nvidia.com/gpugems/gpugems2/part-iii-high-quality-rendering/chapter-20-fast-third-order-texture-filtering + // https://advances.realtimerendering.com/s2021/jpatry_advances2021/index.html#/111/0/2 +#ifdef LIGHTMAP_BICUBIC_SAMPLING + let texture_size = vec2(lightmap_size(lightmap_slot)); + let texel_size = 1.0 / texture_size; + let puv = lightmap_uv * texture_size + 0.5; + let iuv = floor(puv); + let fuv = fract(puv); + let g0x = g0(fuv.x); + let g1x = g1(fuv.x); + let h0x = h0_approx(fuv.x); + let h1x = h1_approx(fuv.x); + let h0y = h0_approx(fuv.y); + let h1y = h1_approx(fuv.y); + let p0 = (vec2(iuv.x + h0x, iuv.y + h0y) - 0.5) * texel_size; + let p1 = (vec2(iuv.x + h1x, iuv.y + h0y) - 0.5) * texel_size; + let p2 = (vec2(iuv.x + h0x, iuv.y + h1y) - 0.5) * texel_size; + let p3 = (vec2(iuv.x + h1x, iuv.y + h1y) - 0.5) * texel_size; + let color = g0(fuv.y) * (g0x * sample(p0, lightmap_slot) + g1x * sample(p1, lightmap_slot)) + g1(fuv.y) * (g0x * sample(p2, lightmap_slot) + g1x * sample(p3, lightmap_slot)); +#else + let color = sample(lightmap_uv, lightmap_slot); +#endif + + return color * exposure; +} + +fn lightmap_size(lightmap_slot: u32) -> vec2 { +#ifdef MULTIPLE_LIGHTMAPS_IN_ARRAY + return textureDimensions(lightmaps_textures[lightmap_slot]); +#else + return textureDimensions(lightmaps_texture); +#endif +} + +fn sample(uv: vec2, lightmap_slot: u32) -> vec3 { // Mipmapping lightmaps is usually a bad idea due to leaking across UV // islands, so there's no harm in using mip level 0 and it lets us avoid // control flow uniformity problems. - // - // TODO(pcwalton): Consider bicubic filtering. #ifdef MULTIPLE_LIGHTMAPS_IN_ARRAY - let lightmap_slot = mesh[instance_index].material_and_lightmap_bind_group_slot >> 16u; - return textureSampleLevel( - lightmaps_textures[lightmap_slot], - lightmaps_samplers[lightmap_slot], - lightmap_uv, - 0.0 - ).rgb * exposure; -#else // MULTIPLE_LIGHTMAPS_IN_ARRAY - return textureSampleLevel( - lightmaps_texture, - lightmaps_sampler, - lightmap_uv, - 0.0 - ).rgb * exposure; -#endif // MULTIPLE_LIGHTMAPS_IN_ARRAY + return textureSampleLevel(lightmaps_textures[lightmap_slot], lightmaps_samplers[lightmap_slot], uv, 0.0).rgb; +#else + return textureSampleLevel(lightmaps_texture, lightmaps_sampler, uv, 0.0).rgb; +#endif +} + +fn w0(a: f32) -> f32 { + return (1.0 / 6.0) * (a * (a * (-a + 3.0) - 3.0) + 1.0); +} + +fn w1(a: f32) -> f32 { + return (1.0 / 6.0) * (a * a * (3.0 * a - 6.0) + 4.0); +} + +fn w2(a: f32) -> f32 { + return (1.0 / 6.0) * (a * (a * (-3.0 * a + 3.0) + 3.0) + 1.0); +} + +fn w3(a: f32) -> f32 { + return (1.0 / 6.0) * (a * a * a); +} + +fn g0(a: f32) -> f32 { + return w0(a) + w1(a); +} + +fn g1(a: f32) -> f32 { + return w2(a) + w3(a); +} + +fn h0_approx(a: f32) -> f32 { + return -0.2 - a * (0.24 * a - 0.44); +} + +fn h1_approx(a: f32) -> f32 { + return 1.0 + a * (0.24 * a - 0.04); } diff --git a/crates/bevy_pbr/src/lightmap/mod.rs b/crates/bevy_pbr/src/lightmap/mod.rs index 31447017c5..3a7b8aa3ad 100644 --- a/crates/bevy_pbr/src/lightmap/mod.rs +++ b/crates/bevy_pbr/src/lightmap/mod.rs @@ -100,6 +100,13 @@ pub struct Lightmap { /// This field allows lightmaps for a variety of meshes to be packed into a /// single atlas. pub uv_rect: Rect, + + /// Whether bicubic sampling should be used for sampling this lightmap. + /// + /// Bicubic sampling is higher quality, but slower, and may lead to light leaks. + /// + /// If true, the lightmap texture's sampler must be set to [`bevy_image::ImageSampler::linear`]. + pub bicubic_sampling: bool, } /// Lightmap data stored in the render world. @@ -126,6 +133,9 @@ pub(crate) struct RenderLightmap { /// /// If bindless lightmaps aren't in use, this will be 0. pub(crate) slot_index: LightmapSlotIndex, + + // Whether or not bicubic sampling should be used for this lightmap. + pub(crate) bicubic_sampling: bool, } /// Stores data for all lightmaps in the render world. @@ -237,6 +247,7 @@ fn extract_lightmaps( lightmap.uv_rect, slab_index, slot_index, + lightmap.bicubic_sampling, ), ); @@ -296,12 +307,14 @@ impl RenderLightmap { uv_rect: Rect, slab_index: LightmapSlabIndex, slot_index: LightmapSlotIndex, + bicubic_sampling: bool, ) -> Self { Self { image, uv_rect, slab_index, slot_index, + bicubic_sampling, } } } @@ -327,6 +340,7 @@ impl Default for Lightmap { Self { image: Default::default(), uv_rect: Rect::new(0.0, 0.0, 1.0, 1.0), + bicubic_sampling: false, } } } diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 5324d77d1f..c39c9c89b6 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -804,12 +804,14 @@ pub fn queue_material_meshes( | MeshPipelineKey::from_bits_retain(mesh.key_bits.bits()) | mesh_pipeline_key_bits; - let lightmap_slab_index = render_lightmaps - .render_lightmaps - .get(visible_entity) - .map(|lightmap| lightmap.slab_index); - if lightmap_slab_index.is_some() { + let mut lightmap_slab = None; + if let Some(lightmap) = render_lightmaps.render_lightmaps.get(visible_entity) { + lightmap_slab = Some(*lightmap.slab_index); mesh_key |= MeshPipelineKey::LIGHTMAPPED; + + if lightmap.bicubic_sampling { + mesh_key |= MeshPipelineKey::LIGHTMAP_BICUBIC_SAMPLING; + } } if render_visibility_ranges.entity_has_crossfading_visibility_ranges(*visible_entity) { @@ -875,8 +877,7 @@ pub fn queue_material_meshes( material_bind_group_index: Some(material.binding.group.0), vertex_slab: vertex_slab.unwrap_or_default(), index_slab, - lightmap_slab: lightmap_slab_index - .map(|lightmap_slab_index| *lightmap_slab_index), + lightmap_slab, }; let bin_key = Opaque3dBinKey { asset_id: mesh_instance.mesh_asset_id.into(), diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index ad3c366c7a..6b0c319a2d 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -488,6 +488,12 @@ where if key.mesh_key.contains(MeshPipelineKey::LIGHTMAPPED) { shader_defs.push("LIGHTMAP".into()); } + if key + .mesh_key + .contains(MeshPipelineKey::LIGHTMAP_BICUBIC_SAMPLING) + { + shader_defs.push("LIGHTMAP_BICUBIC_SAMPLING".into()); + } if layout.0.contains(Mesh::ATTRIBUTE_COLOR) { shader_defs.push("VERTEX_COLORS".into()); @@ -911,12 +917,17 @@ pub fn queue_prepass_material_meshes( mesh_key |= MeshPipelineKey::DEFERRED_PREPASS; } - let lightmap_slab_index = render_lightmaps - .render_lightmaps - .get(visible_entity) - .map(|lightmap| lightmap.slab_index); - if lightmap_slab_index.is_some() { + if let Some(lightmap) = render_lightmaps.render_lightmaps.get(visible_entity) { + // Even though we don't use the lightmap in the forward prepass, the + // `SetMeshBindGroup` render command will bind the data for it. So + // we need to include the appropriate flag in the mesh pipeline key + // to ensure that the necessary bind group layout entries are + // present. mesh_key |= MeshPipelineKey::LIGHTMAPPED; + + if lightmap.bicubic_sampling && deferred { + mesh_key |= MeshPipelineKey::LIGHTMAP_BICUBIC_SAMPLING; + } } if render_visibility_ranges.entity_has_crossfading_visibility_ranges(*visible_entity) { diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 0697630480..6df24bbaa8 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -1810,12 +1810,13 @@ bitflags::bitflags! { const TEMPORAL_JITTER = 1 << 11; const READS_VIEW_TRANSMISSION_TEXTURE = 1 << 12; const LIGHTMAPPED = 1 << 13; - const IRRADIANCE_VOLUME = 1 << 14; - const VISIBILITY_RANGE_DITHER = 1 << 15; - const SCREEN_SPACE_REFLECTIONS = 1 << 16; - const HAS_PREVIOUS_SKIN = 1 << 17; - const HAS_PREVIOUS_MORPH = 1 << 18; - const OIT_ENABLED = 1 << 19; + const LIGHTMAP_BICUBIC_SAMPLING = 1 << 14; + const IRRADIANCE_VOLUME = 1 << 15; + const VISIBILITY_RANGE_DITHER = 1 << 16; + const SCREEN_SPACE_REFLECTIONS = 1 << 17; + const HAS_PREVIOUS_SKIN = 1 << 18; + const HAS_PREVIOUS_MORPH = 1 << 19; + const OIT_ENABLED = 1 << 20; const LAST_FLAG = Self::OIT_ENABLED.bits(); // Bitfields @@ -2239,6 +2240,9 @@ impl SpecializedMeshPipeline for MeshPipeline { if key.contains(MeshPipelineKey::LIGHTMAPPED) { shader_defs.push("LIGHTMAP".into()); } + if key.contains(MeshPipelineKey::LIGHTMAP_BICUBIC_SAMPLING) { + shader_defs.push("LIGHTMAP_BICUBIC_SAMPLING".into()); + } if key.contains(MeshPipelineKey::TEMPORAL_JITTER) { shader_defs.push("TEMPORAL_JITTER".into()); diff --git a/examples/3d/lightmaps.rs b/examples/3d/lightmaps.rs index 0a2a7bcad2..975b37d7f2 100644 --- a/examples/3d/lightmaps.rs +++ b/examples/3d/lightmaps.rs @@ -13,6 +13,9 @@ struct Args { /// enables deferred shading #[argh(switch)] deferred: bool, + /// enables bicubic filtering + #[argh(switch)] + bicubic: bool, } fn main() { @@ -63,6 +66,7 @@ fn add_lightmaps_to_meshes( (Entity, &Name, &MeshMaterial3d), (With, Without), >, + args: Res, ) { let exposure = 250.0; for (entity, name, material) in meshes.iter() { @@ -70,6 +74,7 @@ fn add_lightmaps_to_meshes( materials.get_mut(material).unwrap().lightmap_exposure = exposure; commands.entity(entity).insert(Lightmap { image: asset_server.load("lightmaps/CornellBox-Large.zstd.ktx2"), + bicubic_sampling: args.bicubic, ..default() }); continue; @@ -79,6 +84,7 @@ fn add_lightmaps_to_meshes( materials.get_mut(material).unwrap().lightmap_exposure = exposure; commands.entity(entity).insert(Lightmap { image: asset_server.load("lightmaps/CornellBox-Small.zstd.ktx2"), + bicubic_sampling: args.bicubic, ..default() }); continue; @@ -88,6 +94,7 @@ fn add_lightmaps_to_meshes( materials.get_mut(material).unwrap().lightmap_exposure = exposure; commands.entity(entity).insert(Lightmap { image: asset_server.load("lightmaps/CornellBox-Box.zstd.ktx2"), + bicubic_sampling: args.bicubic, ..default() }); continue; diff --git a/examples/3d/mixed_lighting.rs b/examples/3d/mixed_lighting.rs index 0537f20eae..80d139714a 100644 --- a/examples/3d/mixed_lighting.rs +++ b/examples/3d/mixed_lighting.rs @@ -267,6 +267,7 @@ fn update_lightmaps( commands.entity(entity).insert(Lightmap { image: (*lightmap).clone(), uv_rect, + bicubic_sampling: false, }); } None => { @@ -290,6 +291,7 @@ fn update_lightmaps( commands.entity(entity).insert(Lightmap { image: (*lightmap).clone(), uv_rect: SPHERE_UV_RECT, + bicubic_sampling: false, }); } _ => {