Skip to content

Commit 038e305

Browse files
authored
feature: physically based bloom (#103)
1 parent 3785ba1 commit 038e305

75 files changed

Lines changed: 3121 additions & 2011 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

DEVLOG.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,33 @@
11
# devlog
22

3-
## Tue Apr 9,
3+
## Thu Apr 25, 2024
4+
5+
I missed the intro meeting for NLnet grantees :(.
6+
I realized that I'm just no good at timezones. I'm so used to talking with folks in
7+
LA and SF (and I'm in NZ) that I just assumed our meeting would cross the international
8+
date line, and I got the date wrong! The NLnet folks assured me that it's ok,
9+
but I was really looking forward to meeting the other project developers.
10+
11+
Anyway - I've been putting together the development plan and the software bill of materials
12+
as part of the intake process for the NLnet grant. It's a lot of crossing `T`s and dotting ...
13+
lower case `J`s, but the project will be so much better organized for it.
14+
15+
## Wed Apr 24, 2024 🎉
16+
17+
NLnet is officially sponsoring the development of `renderling`!
18+
19+
In fact, the project was specifically mentioned in
20+
[their announcement](https://nlnet.nl/news/2024/20240417-announcing-projects.html),
21+
which feels good.
22+
23+
[Here is the renderling project overview on NLnet](https://nlnet.nl/project/Renderling/).
24+
25+
Now I've got to get on my project organization and write up some documents, etc, then I
26+
can get started adding atomics to `naga`, and unblock `renderling`'s occlusion culling
27+
and light tiling steps (they will be `rust-gpu` compiled compute shaders, but they require
28+
support for atomics, which `wgpu` currently lacks).
29+
30+
## Tue Apr 9, 2024
431

532
### Better debugging
633

crates/renderling/Cargo.toml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ readme = "../../README.md"
1515
crate-type = ["lib", "dylib"]
1616

1717
[features]
18-
default = ["gltf", "sdf", "shaders", "tutorial", "winit"]
18+
default = ["gltf", "sdf", "shaders", "winit"]
1919
shaders = [
2020
"array_test",
21+
"bloom",
2122
"brdf_lut_convolution_fragment",
2223
"brdf_lut_convolution_vertex",
2324
"generate_mipmap_fragment",
@@ -43,8 +44,18 @@ tutorial = [
4344
"tutorial_slabbed_vertices",
4445
"tutorial_slabbed_renderlet"
4546
]
47+
bloom = [
48+
"bloom_downsample_fragment",
49+
"bloom_upsample_fragment",
50+
"bloom_mix_fragment",
51+
"bloom_vertex"
52+
]
4653
# shaders
4754
array_test = []
55+
bloom_downsample_fragment = []
56+
bloom_upsample_fragment = []
57+
bloom_mix_fragment = []
58+
bloom_vertex = []
4859
brdf_lut_convolution_fragment = []
4960
brdf_lut_convolution_vertex = []
5061
generate_mipmap_fragment = []

crates/renderling/src/atlas/cpu.rs

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ impl RepackPreview {
108108
}
109109

110110
/// A texture atlas, used to store all the textures in a scene.
111+
// TODO: make Atlas threadsafe
111112
pub struct Atlas {
112113
pub texture: crate::Texture,
113114
pub rects: Vec<crunch::Rect>,
@@ -513,7 +514,7 @@ mod test {
513514
atlas::{AtlasTexture, TextureAddressMode, TextureModes},
514515
pbr::Material,
515516
stage::Vertex,
516-
Camera, Renderlet, Renderling, Transform,
517+
Camera, Context, Renderlet, Transform,
517518
};
518519
use crabslab::GrowableSlab;
519520
use glam::{Vec2, Vec3, Vec4};
@@ -522,7 +523,7 @@ mod test {
522523

523524
#[test]
524525
fn can_merge_atlas() {
525-
let r = Renderling::headless(100, 100);
526+
let r = Context::headless(100, 100);
526527
let (device, queue) = r.get_device_and_queue_owned();
527528
println!("{}", std::env::current_dir().unwrap().display());
528529
let cheetah = AtlasImage::from_path("../../img/cheetah.jpg").unwrap();
@@ -538,10 +539,10 @@ mod test {
538539
#[test]
539540
// Ensures that textures are packed and rendered correctly.
540541
fn atlas_uv_mapping() {
541-
let mut r =
542-
Renderling::headless(32, 32).with_background_color(Vec3::splat(0.0).extend(1.0));
543-
let mut stage = r.new_stage();
544-
stage.configure_graph(&mut r, true);
542+
let ctx = Context::headless(32, 32);
543+
let mut stage = ctx
544+
.new_stage()
545+
.with_background_color(Vec3::splat(0.0).extend(1.0));
545546
let (projection, view) = crate::camera::default_ortho2d(32.0, 32.0);
546547
let camera = stage.append(&Camera::new(projection, view));
547548
let dirt = AtlasImage::from_path("../../img/dirt.jpg").unwrap();
@@ -584,7 +585,9 @@ mod test {
584585
..Default::default()
585586
});
586587

587-
let img = r.render_image().unwrap();
588+
let frame = ctx.get_current_frame().unwrap();
589+
stage.render(&frame.view());
590+
let img = frame.read_image().unwrap();
588591
img_diff::assert_img_eq("atlas/uv_mapping.png", img);
589592
}
590593

@@ -597,9 +600,10 @@ mod test {
597600
let sheet_h = icon_h * 3;
598601
let w = sheet_w * 3 + 2;
599602
let h = sheet_h;
600-
let mut r = Renderling::headless(w, h).with_background_color(Vec4::new(1.0, 1.0, 0.0, 1.0));
601-
let mut stage = r.new_stage();
602-
stage.configure_graph(&mut r, true);
603+
let ctx = Context::headless(w, h);
604+
let mut stage = ctx
605+
.new_stage()
606+
.with_background_color(Vec4::new(1.0, 1.0, 0.0, 1.0));
603607
let (projection, view) = crate::camera::default_ortho2d(w as f32, h as f32);
604608
let camera = stage.append(&Camera::new(projection, view));
605609
let dirt = AtlasImage::from_path("../../img/dirt.jpg").unwrap();
@@ -686,7 +690,9 @@ mod test {
686690
})
687691
};
688692

689-
let img = r.render_image().unwrap();
693+
let frame = ctx.get_current_frame().unwrap();
694+
stage.render(&frame.view());
695+
let img = frame.read_image().unwrap();
690696
img_diff::assert_img_eq("atlas/uv_wrapping.png", img);
691697
}
692698

@@ -699,9 +705,10 @@ mod test {
699705
let sheet_h = icon_h * 3;
700706
let w = sheet_w * 3 + 2;
701707
let h = sheet_h;
702-
let mut r = Renderling::headless(w, h).with_background_color(Vec4::new(1.0, 1.0, 0.0, 1.0));
703-
let mut stage = r.new_stage();
704-
stage.configure_graph(&mut r, true);
708+
let ctx = Context::headless(w, h);
709+
let mut stage = ctx
710+
.new_stage()
711+
.with_background_color(Vec4::new(1.0, 1.0, 0.0, 1.0));
705712

706713
let (projection, view) = crate::camera::default_ortho2d(w as f32, h as f32);
707714
let camera = stage.append(&Camera {
@@ -800,7 +807,9 @@ mod test {
800807
})
801808
};
802809

803-
let img = r.render_image().unwrap();
810+
let frame = ctx.get_current_frame().unwrap();
811+
stage.render(&frame.view());
812+
let img = frame.read_image().unwrap();
804813
img_diff::assert_img_eq("atlas/negative_uv_wrapping.png", img);
805814
}
806815

crates/renderling/src/bloom.rs

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
//! Physically based bloom.
2+
//!
3+
//! As described in [learnopengl.com's Physically Based Bloom article](https://learnopengl.com/Guest-Articles/2022/Phys.-Based-Bloom).
4+
use crabslab::{Id, Slab, SlabItem};
5+
use glam::{UVec2, Vec2, Vec4, Vec4Swizzles};
6+
use spirv_std::{image::Image2d, spirv, Sampler};
7+
8+
#[cfg(not(target_arch = "spirv"))]
9+
mod cpu;
10+
#[cfg(not(target_arch = "spirv"))]
11+
pub use cpu::*;
12+
13+
#[derive(Clone, Copy, SlabItem)]
14+
pub struct BloomConfig {
15+
pub resolution: UVec2,
16+
pub upsample_filter_radius: Vec2,
17+
}
18+
19+
impl Default for BloomConfig {
20+
fn default() -> Self {
21+
Self {
22+
resolution: UVec2::ONE,
23+
upsample_filter_radius: Vec2::ONE,
24+
}
25+
}
26+
}
27+
28+
#[cfg(feature = "bloom_vertex")]
29+
/// A passthru vertex shader to facilitate a bloom effect.
30+
#[spirv(vertex)]
31+
pub fn bloom_vertex(
32+
#[spirv(vertex_index)] vertex_index: u32,
33+
#[spirv(instance_index)] in_id: u32,
34+
out_uv: &mut Vec2,
35+
#[spirv(flat)] out_id: &mut u32,
36+
#[spirv(position)] out_clip_pos: &mut Vec4,
37+
) {
38+
let i = (vertex_index % 6) as usize;
39+
*out_uv = crate::math::UV_COORD_QUAD_CCW[i];
40+
*out_clip_pos = crate::math::CLIP_SPACE_COORD_QUAD_CCW[i];
41+
*out_id = in_id;
42+
}
43+
44+
#[cfg(feature = "bloom_downsample_fragment")]
45+
/// Performs downsampling on a texture.
46+
///
47+
/// As taken from Call Of Duty method - presented at ACM Siggraph 2014.
48+
///
49+
/// This particular method was customly designed to eliminate
50+
/// "pulsating artifacts and temporal stability issues".
51+
#[spirv(fragment)]
52+
pub fn bloom_downsample_fragment(
53+
#[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32],
54+
// Remember to add bilinear minification filter for this texture!
55+
// Remember to use a floating-point texture format (for HDR)!
56+
// Remember to use edge clamping for this texture!
57+
#[spirv(descriptor_set = 0, binding = 1)] texture: &Image2d,
58+
#[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler,
59+
in_uv: Vec2,
60+
#[spirv(flat)] in_pixel_size_id: Id<Vec2>,
61+
// frag_color
62+
downsample: &mut Vec4,
63+
) {
64+
use glam::Vec3;
65+
66+
let Vec2 { x, y } = slab.read(in_pixel_size_id);
67+
68+
// Take 13 samples around current texel:
69+
// a - b - c
70+
// - j - k -
71+
// d - e - f
72+
// - l - m -
73+
// g - h - i
74+
// === ('e' is the current texel) ===
75+
let a = texture.sample(*sampler, Vec2::new(in_uv.x - 2.0 * x, in_uv.y + 2.0 * y));
76+
let b = texture.sample(*sampler, Vec2::new(in_uv.x, in_uv.y + 2.0 * y));
77+
let c = texture.sample(*sampler, Vec2::new(in_uv.x + 2.0 * x, in_uv.y + 2.0 * y));
78+
79+
let d = texture.sample(*sampler, Vec2::new(in_uv.x - 2.0 * x, in_uv.y));
80+
let e = texture.sample(*sampler, Vec2::new(in_uv.x, in_uv.y));
81+
let f = texture.sample(*sampler, Vec2::new(in_uv.x + 2.0 * x, in_uv.y));
82+
83+
let g = texture.sample(*sampler, Vec2::new(in_uv.x - 2.0 * x, in_uv.y - 2.0 * y));
84+
let h = texture.sample(*sampler, Vec2::new(in_uv.x, in_uv.y - 2.0 * y));
85+
let i = texture.sample(*sampler, Vec2::new(in_uv.x + 2.0 * x, in_uv.y - 2.0 * y));
86+
87+
let j = texture.sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y + y));
88+
let k = texture.sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y + y));
89+
let l = texture.sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y - y));
90+
let m = texture.sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y - y));
91+
92+
// Apply weighted distribution:
93+
// 0.5 + 0.125 + 0.125 + 0.125 + 0.125 = 1
94+
// a,b,d,e * 0.125
95+
// b,c,e,f * 0.125
96+
// d,e,g,h * 0.125
97+
// e,f,h,i * 0.125
98+
// j,k,l,m * 0.5
99+
// This shows 5 square areas that are being sampled. But some of them overlap,
100+
// so to have an energy preserving downsample we need to make some adjustments.
101+
// The weights are the distributed so that the sum of j,k,l,m (e.g.)
102+
// contribute 0.5 to the final color output. The code below is written
103+
// to effectively yield this sum. We get:
104+
// 0.125*5 + 0.03125*4 + 0.0625*4 = 1
105+
let f1 = 0.125;
106+
let f2 = 0.0625;
107+
let f3 = 0.03125;
108+
let center = e * f1;
109+
let inner = (j + k + l + m) * f1;
110+
let outer = (b + d + h + f) * f2;
111+
let furthest = (a + c + g + i) * f3;
112+
let min = Vec3::splat(f32::EPSILON).extend(1.0);
113+
*downsample = (center + inner + outer + furthest).max(min);
114+
}
115+
116+
#[cfg(feature = "bloom_upsample_fragment")]
117+
/// This shader performs upsampling on a texture.
118+
/// Taken from Call Of Duty method, presented at ACM Siggraph 2014.
119+
#[spirv(fragment)]
120+
pub fn bloom_upsample_fragment(
121+
#[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32],
122+
// Remember to add bilinear minification filter for this texture!
123+
// Remember to use a floating-point texture format (for HDR)!
124+
// Remember to use edge clamping for this texture!
125+
#[spirv(descriptor_set = 0, binding = 1)] texture: &Image2d,
126+
#[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler,
127+
in_uv: Vec2,
128+
#[spirv(flat)] filter_radius_id: Id<Vec2>,
129+
// frag_color
130+
upsample: &mut Vec4,
131+
) {
132+
// The filter kernel is applied with a radius, specified in texture
133+
// coordinates, so that the radius will vary across mip resolutions.
134+
let Vec2 { x, y } = slab.read(filter_radius_id);
135+
136+
// Take 9 samples around current texel:
137+
// a - b - c
138+
// d - e - f
139+
// g - h - i
140+
// === ('e' is the current texel) ===
141+
let a = texture
142+
.sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y + y))
143+
.xyz();
144+
let b = texture
145+
.sample(*sampler, Vec2::new(in_uv.x, in_uv.y + y))
146+
.xyz();
147+
let c = texture
148+
.sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y + y))
149+
.xyz();
150+
151+
let d = texture
152+
.sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y))
153+
.xyz();
154+
let e = texture.sample(*sampler, Vec2::new(in_uv.x, in_uv.y)).xyz();
155+
let f = texture
156+
.sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y))
157+
.xyz();
158+
159+
let g = texture
160+
.sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y - y))
161+
.xyz();
162+
let h = texture
163+
.sample(*sampler, Vec2::new(in_uv.x, in_uv.y - y))
164+
.xyz();
165+
let i = texture
166+
.sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y - y))
167+
.xyz();
168+
169+
// Apply weighted distribution, by using a 3x3 tent filter:
170+
// 1 | 1 2 1 |
171+
// -- * | 2 4 2 |
172+
// 16 | 1 2 1 |
173+
let mut sample = e * 4.0;
174+
sample += (b + d + f + h) * 2.0;
175+
sample += a + c + g + i;
176+
sample *= 1.0 / 16.0;
177+
*upsample = sample.extend(0.5);
178+
}
179+
180+
#[cfg(feature = "bloom_mix_fragment")]
181+
#[spirv(fragment)]
182+
pub fn bloom_mix_fragment(
183+
#[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32],
184+
#[spirv(descriptor_set = 0, binding = 1)] hdr_texture: &Image2d,
185+
#[spirv(descriptor_set = 0, binding = 2)] hdr_sampler: &Sampler,
186+
#[spirv(descriptor_set = 0, binding = 3)] bloom_texture: &Image2d,
187+
#[spirv(descriptor_set = 0, binding = 4)] bloom_sampler: &Sampler,
188+
in_uv: Vec2,
189+
#[spirv(flat)] in_bloom_strength_id: Id<f32>,
190+
frag_color: &mut Vec4,
191+
) {
192+
let bloom_strength = slab.read(in_bloom_strength_id);
193+
let hdr = hdr_texture.sample(*hdr_sampler, in_uv).xyz();
194+
let bloom = bloom_texture.sample(*bloom_sampler, in_uv).xyz();
195+
let color = hdr.lerp(bloom, bloom_strength);
196+
*frag_color = color.extend(1.0)
197+
}

0 commit comments

Comments
 (0)