Higher quality bicubic lightmap sampling (#16740)
# Objective - Closes https://github.com/bevyengine/bevy/issues/14322. ## Solution - Implement fast 4-sample bicubic filtering based on this shader toy https://www.shadertoy.com/view/4df3Dn, with a small speedup from a ghost of tushima presentation. ## Testing - Did you test these changes? If so, how? - Ran on lightmapped example. Practically no difference in that scene. - Are there any parts that need more testing? - Lightmapping a better scene. ## Changelog - Lightmaps now have a higher quality bicubic sampling method (off by default). --------- Co-authored-by: Patrick Walton <pcwalton@mimiga.net>
This commit is contained in:
		
							parent
							
								
									e808fbe987
								
							
						
					
					
						commit
						bb0a82b9a7
					
				| @ -13,33 +13,87 @@ | ||||
| // Samples the lightmap, if any, and returns indirect illumination from it. | ||||
| fn lightmap(uv: vec2<f32>, exposure: f32, instance_index: u32) -> vec3<f32> { | ||||
|     let packed_uv_rect = mesh[instance_index].lightmap_uv_rect; | ||||
|     let uv_rect = vec4<f32>(vec4<u32>( | ||||
|         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<f32>( | ||||
|         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<f32>(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<u32> { | ||||
| #ifdef MULTIPLE_LIGHTMAPS_IN_ARRAY | ||||
|     return textureDimensions(lightmaps_textures[lightmap_slot]); | ||||
| #else | ||||
|     return textureDimensions(lightmaps_texture); | ||||
| #endif | ||||
| } | ||||
| 
 | ||||
| fn sample(uv: vec2<f32>, lightmap_slot: u32) -> vec3<f32> { | ||||
|     // 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); | ||||
| } | ||||
|  | ||||
| @ -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, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -804,12 +804,14 @@ pub fn queue_material_meshes<M: Material>( | ||||
|                 | 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<M: Material>( | ||||
|                             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(), | ||||
|  | ||||
| @ -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<M: Material>( | ||||
|                 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) { | ||||
|  | ||||
| @ -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()); | ||||
|  | ||||
| @ -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<StandardMaterial>), | ||||
|         (With<Mesh3d>, Without<Lightmap>), | ||||
|     >, | ||||
|     args: Res<Args>, | ||||
| ) { | ||||
|     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; | ||||
|  | ||||
| @ -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, | ||||
|                     }); | ||||
|                 } | ||||
|                 _ => { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 JMS55
						JMS55