Skip to content

Commit

Permalink
Higher quality bicubic lightmap sampling (#16740)
Browse files Browse the repository at this point in the history
# Objective
- Closes #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 <[email protected]>
  • Loading branch information
JMS55 and pcwalton authored Jan 12, 2025
1 parent e808fbe commit bb0a82b
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 41 deletions.
100 changes: 77 additions & 23 deletions crates/bevy_pbr/src/lightmap/lightmap.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
14 changes: 14 additions & 0 deletions crates/bevy_pbr/src/lightmap/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -237,6 +247,7 @@ fn extract_lightmaps(
lightmap.uv_rect,
slab_index,
slot_index,
lightmap.bicubic_sampling,
),
);

Expand Down Expand Up @@ -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,
}
}
}
Expand All @@ -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,
}
}
}
Expand Down
15 changes: 8 additions & 7 deletions crates/bevy_pbr/src/material.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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(),
Expand Down
21 changes: 16 additions & 5 deletions crates/bevy_pbr/src/prepass/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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) {
Expand Down
16 changes: 10 additions & 6 deletions crates/bevy_pbr/src/render/mesh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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());
Expand Down
7 changes: 7 additions & 0 deletions examples/3d/lightmaps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ struct Args {
/// enables deferred shading
#[argh(switch)]
deferred: bool,
/// enables bicubic filtering
#[argh(switch)]
bicubic: bool,
}

fn main() {
Expand Down Expand Up @@ -63,13 +66,15 @@ 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() {
if &**name == "large_box" {
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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions examples/3d/mixed_lighting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ fn update_lightmaps(
commands.entity(entity).insert(Lightmap {
image: (*lightmap).clone(),
uv_rect,
bicubic_sampling: false,
});
}
None => {
Expand All @@ -290,6 +291,7 @@ fn update_lightmaps(
commands.entity(entity).insert(Lightmap {
image: (*lightmap).clone(),
uv_rect: SPHERE_UV_RECT,
bicubic_sampling: false,
});
}
_ => {
Expand Down

0 comments on commit bb0a82b

Please sign in to comment.