Skip to content

Commit bb0a82b

Browse files
JMS55pcwalton
andauthored
Higher quality bicubic lightmap sampling (#16740)
# 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]>
1 parent e808fbe commit bb0a82b

File tree

7 files changed

+134
-41
lines changed

7 files changed

+134
-41
lines changed

crates/bevy_pbr/src/lightmap/lightmap.wgsl

Lines changed: 77 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,33 +13,87 @@
1313
// Samples the lightmap, if any, and returns indirect illumination from it.
1414
fn lightmap(uv: vec2<f32>, exposure: f32, instance_index: u32) -> vec3<f32> {
1515
let packed_uv_rect = mesh[instance_index].lightmap_uv_rect;
16-
let uv_rect = vec4<f32>(vec4<u32>(
17-
packed_uv_rect.x & 0xffffu,
18-
packed_uv_rect.x >> 16u,
19-
packed_uv_rect.y & 0xffffu,
20-
packed_uv_rect.y >> 16u)) / 65535.0;
21-
16+
let uv_rect = vec4<f32>(
17+
unpack2x16unorm(packed_uv_rect.x),
18+
unpack2x16unorm(packed_uv_rect.y),
19+
);
2220
let lightmap_uv = mix(uv_rect.xy, uv_rect.zw, uv);
21+
let lightmap_slot = mesh[instance_index].material_and_lightmap_bind_group_slot >> 16u;
22+
23+
// Bicubic 4-tap
24+
// https://developer.nvidia.com/gpugems/gpugems2/part-iii-high-quality-rendering/chapter-20-fast-third-order-texture-filtering
25+
// https://advances.realtimerendering.com/s2021/jpatry_advances2021/index.html#/111/0/2
26+
#ifdef LIGHTMAP_BICUBIC_SAMPLING
27+
let texture_size = vec2<f32>(lightmap_size(lightmap_slot));
28+
let texel_size = 1.0 / texture_size;
29+
let puv = lightmap_uv * texture_size + 0.5;
30+
let iuv = floor(puv);
31+
let fuv = fract(puv);
32+
let g0x = g0(fuv.x);
33+
let g1x = g1(fuv.x);
34+
let h0x = h0_approx(fuv.x);
35+
let h1x = h1_approx(fuv.x);
36+
let h0y = h0_approx(fuv.y);
37+
let h1y = h1_approx(fuv.y);
38+
let p0 = (vec2(iuv.x + h0x, iuv.y + h0y) - 0.5) * texel_size;
39+
let p1 = (vec2(iuv.x + h1x, iuv.y + h0y) - 0.5) * texel_size;
40+
let p2 = (vec2(iuv.x + h0x, iuv.y + h1y) - 0.5) * texel_size;
41+
let p3 = (vec2(iuv.x + h1x, iuv.y + h1y) - 0.5) * texel_size;
42+
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));
43+
#else
44+
let color = sample(lightmap_uv, lightmap_slot);
45+
#endif
46+
47+
return color * exposure;
48+
}
49+
50+
fn lightmap_size(lightmap_slot: u32) -> vec2<u32> {
51+
#ifdef MULTIPLE_LIGHTMAPS_IN_ARRAY
52+
return textureDimensions(lightmaps_textures[lightmap_slot]);
53+
#else
54+
return textureDimensions(lightmaps_texture);
55+
#endif
56+
}
2357

58+
fn sample(uv: vec2<f32>, lightmap_slot: u32) -> vec3<f32> {
2459
// Mipmapping lightmaps is usually a bad idea due to leaking across UV
2560
// islands, so there's no harm in using mip level 0 and it lets us avoid
2661
// control flow uniformity problems.
27-
//
28-
// TODO(pcwalton): Consider bicubic filtering.
2962
#ifdef MULTIPLE_LIGHTMAPS_IN_ARRAY
30-
let lightmap_slot = mesh[instance_index].material_and_lightmap_bind_group_slot >> 16u;
31-
return textureSampleLevel(
32-
lightmaps_textures[lightmap_slot],
33-
lightmaps_samplers[lightmap_slot],
34-
lightmap_uv,
35-
0.0
36-
).rgb * exposure;
37-
#else // MULTIPLE_LIGHTMAPS_IN_ARRAY
38-
return textureSampleLevel(
39-
lightmaps_texture,
40-
lightmaps_sampler,
41-
lightmap_uv,
42-
0.0
43-
).rgb * exposure;
44-
#endif // MULTIPLE_LIGHTMAPS_IN_ARRAY
63+
return textureSampleLevel(lightmaps_textures[lightmap_slot], lightmaps_samplers[lightmap_slot], uv, 0.0).rgb;
64+
#else
65+
return textureSampleLevel(lightmaps_texture, lightmaps_sampler, uv, 0.0).rgb;
66+
#endif
67+
}
68+
69+
fn w0(a: f32) -> f32 {
70+
return (1.0 / 6.0) * (a * (a * (-a + 3.0) - 3.0) + 1.0);
71+
}
72+
73+
fn w1(a: f32) -> f32 {
74+
return (1.0 / 6.0) * (a * a * (3.0 * a - 6.0) + 4.0);
75+
}
76+
77+
fn w2(a: f32) -> f32 {
78+
return (1.0 / 6.0) * (a * (a * (-3.0 * a + 3.0) + 3.0) + 1.0);
79+
}
80+
81+
fn w3(a: f32) -> f32 {
82+
return (1.0 / 6.0) * (a * a * a);
83+
}
84+
85+
fn g0(a: f32) -> f32 {
86+
return w0(a) + w1(a);
87+
}
88+
89+
fn g1(a: f32) -> f32 {
90+
return w2(a) + w3(a);
91+
}
92+
93+
fn h0_approx(a: f32) -> f32 {
94+
return -0.2 - a * (0.24 * a - 0.44);
95+
}
96+
97+
fn h1_approx(a: f32) -> f32 {
98+
return 1.0 + a * (0.24 * a - 0.04);
4599
}

crates/bevy_pbr/src/lightmap/mod.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,13 @@ pub struct Lightmap {
100100
/// This field allows lightmaps for a variety of meshes to be packed into a
101101
/// single atlas.
102102
pub uv_rect: Rect,
103+
104+
/// Whether bicubic sampling should be used for sampling this lightmap.
105+
///
106+
/// Bicubic sampling is higher quality, but slower, and may lead to light leaks.
107+
///
108+
/// If true, the lightmap texture's sampler must be set to [`bevy_image::ImageSampler::linear`].
109+
pub bicubic_sampling: bool,
103110
}
104111

105112
/// Lightmap data stored in the render world.
@@ -126,6 +133,9 @@ pub(crate) struct RenderLightmap {
126133
///
127134
/// If bindless lightmaps aren't in use, this will be 0.
128135
pub(crate) slot_index: LightmapSlotIndex,
136+
137+
// Whether or not bicubic sampling should be used for this lightmap.
138+
pub(crate) bicubic_sampling: bool,
129139
}
130140

131141
/// Stores data for all lightmaps in the render world.
@@ -237,6 +247,7 @@ fn extract_lightmaps(
237247
lightmap.uv_rect,
238248
slab_index,
239249
slot_index,
250+
lightmap.bicubic_sampling,
240251
),
241252
);
242253

@@ -296,12 +307,14 @@ impl RenderLightmap {
296307
uv_rect: Rect,
297308
slab_index: LightmapSlabIndex,
298309
slot_index: LightmapSlotIndex,
310+
bicubic_sampling: bool,
299311
) -> Self {
300312
Self {
301313
image,
302314
uv_rect,
303315
slab_index,
304316
slot_index,
317+
bicubic_sampling,
305318
}
306319
}
307320
}
@@ -327,6 +340,7 @@ impl Default for Lightmap {
327340
Self {
328341
image: Default::default(),
329342
uv_rect: Rect::new(0.0, 0.0, 1.0, 1.0),
343+
bicubic_sampling: false,
330344
}
331345
}
332346
}

crates/bevy_pbr/src/material.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -804,12 +804,14 @@ pub fn queue_material_meshes<M: Material>(
804804
| MeshPipelineKey::from_bits_retain(mesh.key_bits.bits())
805805
| mesh_pipeline_key_bits;
806806

807-
let lightmap_slab_index = render_lightmaps
808-
.render_lightmaps
809-
.get(visible_entity)
810-
.map(|lightmap| lightmap.slab_index);
811-
if lightmap_slab_index.is_some() {
807+
let mut lightmap_slab = None;
808+
if let Some(lightmap) = render_lightmaps.render_lightmaps.get(visible_entity) {
809+
lightmap_slab = Some(*lightmap.slab_index);
812810
mesh_key |= MeshPipelineKey::LIGHTMAPPED;
811+
812+
if lightmap.bicubic_sampling {
813+
mesh_key |= MeshPipelineKey::LIGHTMAP_BICUBIC_SAMPLING;
814+
}
813815
}
814816

815817
if render_visibility_ranges.entity_has_crossfading_visibility_ranges(*visible_entity) {
@@ -875,8 +877,7 @@ pub fn queue_material_meshes<M: Material>(
875877
material_bind_group_index: Some(material.binding.group.0),
876878
vertex_slab: vertex_slab.unwrap_or_default(),
877879
index_slab,
878-
lightmap_slab: lightmap_slab_index
879-
.map(|lightmap_slab_index| *lightmap_slab_index),
880+
lightmap_slab,
880881
};
881882
let bin_key = Opaque3dBinKey {
882883
asset_id: mesh_instance.mesh_asset_id.into(),

crates/bevy_pbr/src/prepass/mod.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,12 @@ where
488488
if key.mesh_key.contains(MeshPipelineKey::LIGHTMAPPED) {
489489
shader_defs.push("LIGHTMAP".into());
490490
}
491+
if key
492+
.mesh_key
493+
.contains(MeshPipelineKey::LIGHTMAP_BICUBIC_SAMPLING)
494+
{
495+
shader_defs.push("LIGHTMAP_BICUBIC_SAMPLING".into());
496+
}
491497

492498
if layout.0.contains(Mesh::ATTRIBUTE_COLOR) {
493499
shader_defs.push("VERTEX_COLORS".into());
@@ -911,12 +917,17 @@ pub fn queue_prepass_material_meshes<M: Material>(
911917
mesh_key |= MeshPipelineKey::DEFERRED_PREPASS;
912918
}
913919

914-
let lightmap_slab_index = render_lightmaps
915-
.render_lightmaps
916-
.get(visible_entity)
917-
.map(|lightmap| lightmap.slab_index);
918-
if lightmap_slab_index.is_some() {
920+
if let Some(lightmap) = render_lightmaps.render_lightmaps.get(visible_entity) {
921+
// Even though we don't use the lightmap in the forward prepass, the
922+
// `SetMeshBindGroup` render command will bind the data for it. So
923+
// we need to include the appropriate flag in the mesh pipeline key
924+
// to ensure that the necessary bind group layout entries are
925+
// present.
919926
mesh_key |= MeshPipelineKey::LIGHTMAPPED;
927+
928+
if lightmap.bicubic_sampling && deferred {
929+
mesh_key |= MeshPipelineKey::LIGHTMAP_BICUBIC_SAMPLING;
930+
}
920931
}
921932

922933
if render_visibility_ranges.entity_has_crossfading_visibility_ranges(*visible_entity) {

crates/bevy_pbr/src/render/mesh.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1810,12 +1810,13 @@ bitflags::bitflags! {
18101810
const TEMPORAL_JITTER = 1 << 11;
18111811
const READS_VIEW_TRANSMISSION_TEXTURE = 1 << 12;
18121812
const LIGHTMAPPED = 1 << 13;
1813-
const IRRADIANCE_VOLUME = 1 << 14;
1814-
const VISIBILITY_RANGE_DITHER = 1 << 15;
1815-
const SCREEN_SPACE_REFLECTIONS = 1 << 16;
1816-
const HAS_PREVIOUS_SKIN = 1 << 17;
1817-
const HAS_PREVIOUS_MORPH = 1 << 18;
1818-
const OIT_ENABLED = 1 << 19;
1813+
const LIGHTMAP_BICUBIC_SAMPLING = 1 << 14;
1814+
const IRRADIANCE_VOLUME = 1 << 15;
1815+
const VISIBILITY_RANGE_DITHER = 1 << 16;
1816+
const SCREEN_SPACE_REFLECTIONS = 1 << 17;
1817+
const HAS_PREVIOUS_SKIN = 1 << 18;
1818+
const HAS_PREVIOUS_MORPH = 1 << 19;
1819+
const OIT_ENABLED = 1 << 20;
18191820
const LAST_FLAG = Self::OIT_ENABLED.bits();
18201821

18211822
// Bitfields
@@ -2239,6 +2240,9 @@ impl SpecializedMeshPipeline for MeshPipeline {
22392240
if key.contains(MeshPipelineKey::LIGHTMAPPED) {
22402241
shader_defs.push("LIGHTMAP".into());
22412242
}
2243+
if key.contains(MeshPipelineKey::LIGHTMAP_BICUBIC_SAMPLING) {
2244+
shader_defs.push("LIGHTMAP_BICUBIC_SAMPLING".into());
2245+
}
22422246

22432247
if key.contains(MeshPipelineKey::TEMPORAL_JITTER) {
22442248
shader_defs.push("TEMPORAL_JITTER".into());

examples/3d/lightmaps.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ struct Args {
1313
/// enables deferred shading
1414
#[argh(switch)]
1515
deferred: bool,
16+
/// enables bicubic filtering
17+
#[argh(switch)]
18+
bicubic: bool,
1619
}
1720

1821
fn main() {
@@ -63,13 +66,15 @@ fn add_lightmaps_to_meshes(
6366
(Entity, &Name, &MeshMaterial3d<StandardMaterial>),
6467
(With<Mesh3d>, Without<Lightmap>),
6568
>,
69+
args: Res<Args>,
6670
) {
6771
let exposure = 250.0;
6872
for (entity, name, material) in meshes.iter() {
6973
if &**name == "large_box" {
7074
materials.get_mut(material).unwrap().lightmap_exposure = exposure;
7175
commands.entity(entity).insert(Lightmap {
7276
image: asset_server.load("lightmaps/CornellBox-Large.zstd.ktx2"),
77+
bicubic_sampling: args.bicubic,
7378
..default()
7479
});
7580
continue;
@@ -79,6 +84,7 @@ fn add_lightmaps_to_meshes(
7984
materials.get_mut(material).unwrap().lightmap_exposure = exposure;
8085
commands.entity(entity).insert(Lightmap {
8186
image: asset_server.load("lightmaps/CornellBox-Small.zstd.ktx2"),
87+
bicubic_sampling: args.bicubic,
8288
..default()
8389
});
8490
continue;
@@ -88,6 +94,7 @@ fn add_lightmaps_to_meshes(
8894
materials.get_mut(material).unwrap().lightmap_exposure = exposure;
8995
commands.entity(entity).insert(Lightmap {
9096
image: asset_server.load("lightmaps/CornellBox-Box.zstd.ktx2"),
97+
bicubic_sampling: args.bicubic,
9198
..default()
9299
});
93100
continue;

examples/3d/mixed_lighting.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ fn update_lightmaps(
267267
commands.entity(entity).insert(Lightmap {
268268
image: (*lightmap).clone(),
269269
uv_rect,
270+
bicubic_sampling: false,
270271
});
271272
}
272273
None => {
@@ -290,6 +291,7 @@ fn update_lightmaps(
290291
commands.entity(entity).insert(Lightmap {
291292
image: (*lightmap).clone(),
292293
uv_rect: SPHERE_UV_RECT,
294+
bicubic_sampling: false,
293295
});
294296
}
295297
_ => {

0 commit comments

Comments
 (0)