Skip to content

Commit

Permalink
feature: physically based bloom (#103)
Browse files Browse the repository at this point in the history
  • Loading branch information
schell authored Apr 30, 2024
1 parent 3785ba1 commit 038e305
Show file tree
Hide file tree
Showing 75 changed files with 3,121 additions and 2,011 deletions.
29 changes: 28 additions & 1 deletion DEVLOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,33 @@
# devlog

## Tue Apr 9,
## Thu Apr 25, 2024

I missed the intro meeting for NLnet grantees :(.
I realized that I'm just no good at timezones. I'm so used to talking with folks in
LA and SF (and I'm in NZ) that I just assumed our meeting would cross the international
date line, and I got the date wrong! The NLnet folks assured me that it's ok,
but I was really looking forward to meeting the other project developers.

Anyway - I've been putting together the development plan and the software bill of materials
as part of the intake process for the NLnet grant. It's a lot of crossing `T`s and dotting ...
lower case `J`s, but the project will be so much better organized for it.

## Wed Apr 24, 2024 🎉

NLnet is officially sponsoring the development of `renderling`!

In fact, the project was specifically mentioned in
[their announcement](https://nlnet.nl/news/2024/20240417-announcing-projects.html),
which feels good.

[Here is the renderling project overview on NLnet](https://nlnet.nl/project/Renderling/).

Now I've got to get on my project organization and write up some documents, etc, then I
can get started adding atomics to `naga`, and unblock `renderling`'s occlusion culling
and light tiling steps (they will be `rust-gpu` compiled compute shaders, but they require
support for atomics, which `wgpu` currently lacks).

## Tue Apr 9, 2024

### Better debugging

Expand Down
13 changes: 12 additions & 1 deletion crates/renderling/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ readme = "../../README.md"
crate-type = ["lib", "dylib"]

[features]
default = ["gltf", "sdf", "shaders", "tutorial", "winit"]
default = ["gltf", "sdf", "shaders", "winit"]
shaders = [
"array_test",
"bloom",
"brdf_lut_convolution_fragment",
"brdf_lut_convolution_vertex",
"generate_mipmap_fragment",
Expand All @@ -43,8 +44,18 @@ tutorial = [
"tutorial_slabbed_vertices",
"tutorial_slabbed_renderlet"
]
bloom = [
"bloom_downsample_fragment",
"bloom_upsample_fragment",
"bloom_mix_fragment",
"bloom_vertex"
]
# shaders
array_test = []
bloom_downsample_fragment = []
bloom_upsample_fragment = []
bloom_mix_fragment = []
bloom_vertex = []
brdf_lut_convolution_fragment = []
brdf_lut_convolution_vertex = []
generate_mipmap_fragment = []
Expand Down
39 changes: 24 additions & 15 deletions crates/renderling/src/atlas/cpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ impl RepackPreview {
}

/// A texture atlas, used to store all the textures in a scene.
// TODO: make Atlas threadsafe
pub struct Atlas {
pub texture: crate::Texture,
pub rects: Vec<crunch::Rect>,
Expand Down Expand Up @@ -513,7 +514,7 @@ mod test {
atlas::{AtlasTexture, TextureAddressMode, TextureModes},
pbr::Material,
stage::Vertex,
Camera, Renderlet, Renderling, Transform,
Camera, Context, Renderlet, Transform,
};
use crabslab::GrowableSlab;
use glam::{Vec2, Vec3, Vec4};
Expand All @@ -522,7 +523,7 @@ mod test {

#[test]
fn can_merge_atlas() {
let r = Renderling::headless(100, 100);
let r = Context::headless(100, 100);
let (device, queue) = r.get_device_and_queue_owned();
println!("{}", std::env::current_dir().unwrap().display());
let cheetah = AtlasImage::from_path("../../img/cheetah.jpg").unwrap();
Expand All @@ -538,10 +539,10 @@ mod test {
#[test]
// Ensures that textures are packed and rendered correctly.
fn atlas_uv_mapping() {
let mut r =
Renderling::headless(32, 32).with_background_color(Vec3::splat(0.0).extend(1.0));
let mut stage = r.new_stage();
stage.configure_graph(&mut r, true);
let ctx = Context::headless(32, 32);
let mut stage = ctx
.new_stage()
.with_background_color(Vec3::splat(0.0).extend(1.0));
let (projection, view) = crate::camera::default_ortho2d(32.0, 32.0);
let camera = stage.append(&Camera::new(projection, view));
let dirt = AtlasImage::from_path("../../img/dirt.jpg").unwrap();
Expand Down Expand Up @@ -584,7 +585,9 @@ mod test {
..Default::default()
});

let img = r.render_image().unwrap();
let frame = ctx.get_current_frame().unwrap();
stage.render(&frame.view());
let img = frame.read_image().unwrap();
img_diff::assert_img_eq("atlas/uv_mapping.png", img);
}

Expand All @@ -597,9 +600,10 @@ mod test {
let sheet_h = icon_h * 3;
let w = sheet_w * 3 + 2;
let h = sheet_h;
let mut r = Renderling::headless(w, h).with_background_color(Vec4::new(1.0, 1.0, 0.0, 1.0));
let mut stage = r.new_stage();
stage.configure_graph(&mut r, true);
let ctx = Context::headless(w, h);
let mut stage = ctx
.new_stage()
.with_background_color(Vec4::new(1.0, 1.0, 0.0, 1.0));
let (projection, view) = crate::camera::default_ortho2d(w as f32, h as f32);
let camera = stage.append(&Camera::new(projection, view));
let dirt = AtlasImage::from_path("../../img/dirt.jpg").unwrap();
Expand Down Expand Up @@ -686,7 +690,9 @@ mod test {
})
};

let img = r.render_image().unwrap();
let frame = ctx.get_current_frame().unwrap();
stage.render(&frame.view());
let img = frame.read_image().unwrap();
img_diff::assert_img_eq("atlas/uv_wrapping.png", img);
}

Expand All @@ -699,9 +705,10 @@ mod test {
let sheet_h = icon_h * 3;
let w = sheet_w * 3 + 2;
let h = sheet_h;
let mut r = Renderling::headless(w, h).with_background_color(Vec4::new(1.0, 1.0, 0.0, 1.0));
let mut stage = r.new_stage();
stage.configure_graph(&mut r, true);
let ctx = Context::headless(w, h);
let mut stage = ctx
.new_stage()
.with_background_color(Vec4::new(1.0, 1.0, 0.0, 1.0));

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

let img = r.render_image().unwrap();
let frame = ctx.get_current_frame().unwrap();
stage.render(&frame.view());
let img = frame.read_image().unwrap();
img_diff::assert_img_eq("atlas/negative_uv_wrapping.png", img);
}

Expand Down
197 changes: 197 additions & 0 deletions crates/renderling/src/bloom.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
//! Physically based bloom.
//!
//! As described in [learnopengl.com's Physically Based Bloom article](https://learnopengl.com/Guest-Articles/2022/Phys.-Based-Bloom).
use crabslab::{Id, Slab, SlabItem};
use glam::{UVec2, Vec2, Vec4, Vec4Swizzles};
use spirv_std::{image::Image2d, spirv, Sampler};

#[cfg(not(target_arch = "spirv"))]
mod cpu;
#[cfg(not(target_arch = "spirv"))]
pub use cpu::*;

#[derive(Clone, Copy, SlabItem)]
pub struct BloomConfig {
pub resolution: UVec2,
pub upsample_filter_radius: Vec2,
}

impl Default for BloomConfig {
fn default() -> Self {
Self {
resolution: UVec2::ONE,
upsample_filter_radius: Vec2::ONE,
}
}
}

#[cfg(feature = "bloom_vertex")]
/// A passthru vertex shader to facilitate a bloom effect.
#[spirv(vertex)]
pub fn bloom_vertex(
#[spirv(vertex_index)] vertex_index: u32,
#[spirv(instance_index)] in_id: u32,
out_uv: &mut Vec2,
#[spirv(flat)] out_id: &mut u32,
#[spirv(position)] out_clip_pos: &mut Vec4,
) {
let i = (vertex_index % 6) as usize;
*out_uv = crate::math::UV_COORD_QUAD_CCW[i];
*out_clip_pos = crate::math::CLIP_SPACE_COORD_QUAD_CCW[i];
*out_id = in_id;
}

#[cfg(feature = "bloom_downsample_fragment")]
/// Performs downsampling on a texture.
///
/// As taken from Call Of Duty method - presented at ACM Siggraph 2014.
///
/// This particular method was customly designed to eliminate
/// "pulsating artifacts and temporal stability issues".
#[spirv(fragment)]
pub fn bloom_downsample_fragment(
#[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32],
// Remember to add bilinear minification filter for this texture!
// Remember to use a floating-point texture format (for HDR)!
// Remember to use edge clamping for this texture!
#[spirv(descriptor_set = 0, binding = 1)] texture: &Image2d,
#[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler,
in_uv: Vec2,
#[spirv(flat)] in_pixel_size_id: Id<Vec2>,
// frag_color
downsample: &mut Vec4,
) {
use glam::Vec3;

let Vec2 { x, y } = slab.read(in_pixel_size_id);

// Take 13 samples around current texel:
// a - b - c
// - j - k -
// d - e - f
// - l - m -
// g - h - i
// === ('e' is the current texel) ===
let a = texture.sample(*sampler, Vec2::new(in_uv.x - 2.0 * x, in_uv.y + 2.0 * y));
let b = texture.sample(*sampler, Vec2::new(in_uv.x, in_uv.y + 2.0 * y));
let c = texture.sample(*sampler, Vec2::new(in_uv.x + 2.0 * x, in_uv.y + 2.0 * y));

let d = texture.sample(*sampler, Vec2::new(in_uv.x - 2.0 * x, in_uv.y));
let e = texture.sample(*sampler, Vec2::new(in_uv.x, in_uv.y));
let f = texture.sample(*sampler, Vec2::new(in_uv.x + 2.0 * x, in_uv.y));

let g = texture.sample(*sampler, Vec2::new(in_uv.x - 2.0 * x, in_uv.y - 2.0 * y));
let h = texture.sample(*sampler, Vec2::new(in_uv.x, in_uv.y - 2.0 * y));
let i = texture.sample(*sampler, Vec2::new(in_uv.x + 2.0 * x, in_uv.y - 2.0 * y));

let j = texture.sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y + y));
let k = texture.sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y + y));
let l = texture.sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y - y));
let m = texture.sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y - y));

// Apply weighted distribution:
// 0.5 + 0.125 + 0.125 + 0.125 + 0.125 = 1
// a,b,d,e * 0.125
// b,c,e,f * 0.125
// d,e,g,h * 0.125
// e,f,h,i * 0.125
// j,k,l,m * 0.5
// This shows 5 square areas that are being sampled. But some of them overlap,
// so to have an energy preserving downsample we need to make some adjustments.
// The weights are the distributed so that the sum of j,k,l,m (e.g.)
// contribute 0.5 to the final color output. The code below is written
// to effectively yield this sum. We get:
// 0.125*5 + 0.03125*4 + 0.0625*4 = 1
let f1 = 0.125;
let f2 = 0.0625;
let f3 = 0.03125;
let center = e * f1;
let inner = (j + k + l + m) * f1;
let outer = (b + d + h + f) * f2;
let furthest = (a + c + g + i) * f3;
let min = Vec3::splat(f32::EPSILON).extend(1.0);
*downsample = (center + inner + outer + furthest).max(min);
}

#[cfg(feature = "bloom_upsample_fragment")]
/// This shader performs upsampling on a texture.
/// Taken from Call Of Duty method, presented at ACM Siggraph 2014.
#[spirv(fragment)]
pub fn bloom_upsample_fragment(
#[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32],
// Remember to add bilinear minification filter for this texture!
// Remember to use a floating-point texture format (for HDR)!
// Remember to use edge clamping for this texture!
#[spirv(descriptor_set = 0, binding = 1)] texture: &Image2d,
#[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler,
in_uv: Vec2,
#[spirv(flat)] filter_radius_id: Id<Vec2>,
// frag_color
upsample: &mut Vec4,
) {
// The filter kernel is applied with a radius, specified in texture
// coordinates, so that the radius will vary across mip resolutions.
let Vec2 { x, y } = slab.read(filter_radius_id);

// Take 9 samples around current texel:
// a - b - c
// d - e - f
// g - h - i
// === ('e' is the current texel) ===
let a = texture
.sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y + y))
.xyz();
let b = texture
.sample(*sampler, Vec2::new(in_uv.x, in_uv.y + y))
.xyz();
let c = texture
.sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y + y))
.xyz();

let d = texture
.sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y))
.xyz();
let e = texture.sample(*sampler, Vec2::new(in_uv.x, in_uv.y)).xyz();
let f = texture
.sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y))
.xyz();

let g = texture
.sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y - y))
.xyz();
let h = texture
.sample(*sampler, Vec2::new(in_uv.x, in_uv.y - y))
.xyz();
let i = texture
.sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y - y))
.xyz();

// Apply weighted distribution, by using a 3x3 tent filter:
// 1 | 1 2 1 |
// -- * | 2 4 2 |
// 16 | 1 2 1 |
let mut sample = e * 4.0;
sample += (b + d + f + h) * 2.0;
sample += a + c + g + i;
sample *= 1.0 / 16.0;
*upsample = sample.extend(0.5);
}

#[cfg(feature = "bloom_mix_fragment")]
#[spirv(fragment)]
pub fn bloom_mix_fragment(
#[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32],
#[spirv(descriptor_set = 0, binding = 1)] hdr_texture: &Image2d,
#[spirv(descriptor_set = 0, binding = 2)] hdr_sampler: &Sampler,
#[spirv(descriptor_set = 0, binding = 3)] bloom_texture: &Image2d,
#[spirv(descriptor_set = 0, binding = 4)] bloom_sampler: &Sampler,
in_uv: Vec2,
#[spirv(flat)] in_bloom_strength_id: Id<f32>,
frag_color: &mut Vec4,
) {
let bloom_strength = slab.read(in_bloom_strength_id);
let hdr = hdr_texture.sample(*hdr_sampler, in_uv).xyz();
let bloom = bloom_texture.sample(*bloom_sampler, in_uv).xyz();
let color = hdr.lerp(bloom, bloom_strength);
*frag_color = color.extend(1.0)
}
Loading

0 comments on commit 038e305

Please sign in to comment.