feat(renderer): add bloom and tonemap shaders
- bloom_shader.wgsl: fullscreen vertex + fs_downsample (5-tap box + bright extract) + fs_upsample (9-tap tent) - tonemap_shader.wgsl: fullscreen vertex + fs_main (HDR+bloom combine, exposure, ACES, gamma 1/2.2) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -38,7 +38,7 @@ impl BloomResources {
|
|||||||
pub fn new(device: &wgpu::Device, width: u32, height: u32) -> Self {
|
pub fn new(device: &wgpu::Device, width: u32, height: u32) -> Self {
|
||||||
let mip_views = create_mip_views(device, width, height);
|
let mip_views = create_mip_views(device, width, height);
|
||||||
|
|
||||||
let uniform = BloomUniform::default();
|
let _uniform = BloomUniform::default();
|
||||||
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
label: Some("Bloom Uniform Buffer"),
|
label: Some("Bloom Uniform Buffer"),
|
||||||
size: std::mem::size_of::<BloomUniform>() as u64,
|
size: std::mem::size_of::<BloomUniform>() as u64,
|
||||||
|
|||||||
112
crates/voltex_renderer/src/bloom_shader.wgsl
Normal file
112
crates/voltex_renderer/src/bloom_shader.wgsl
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
// Bloom pass shader.
|
||||||
|
// Two fragment entry points:
|
||||||
|
// fs_downsample — 5-tap box filter with optional bright extraction
|
||||||
|
// fs_upsample — 9-tap tent filter for the upsample chain
|
||||||
|
|
||||||
|
// ── Group 0 ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@group(0) @binding(0) var t_input: texture_2d<f32>;
|
||||||
|
@group(0) @binding(1) var s_input: sampler;
|
||||||
|
|
||||||
|
struct BloomUniform {
|
||||||
|
threshold: f32,
|
||||||
|
soft_threshold: f32,
|
||||||
|
_padding: vec2<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
@group(0) @binding(2) var<uniform> bloom: BloomUniform;
|
||||||
|
|
||||||
|
// ── Vertex stage ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
struct VertexInput {
|
||||||
|
@location(0) position: vec2<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) clip_position: vec4<f32>,
|
||||||
|
@location(0) uv: vec2<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_main(v: VertexInput) -> VertexOutput {
|
||||||
|
var out: VertexOutput;
|
||||||
|
out.clip_position = vec4<f32>(v.position, 0.0, 1.0);
|
||||||
|
out.uv = vec2<f32>(v.position.x * 0.5 + 0.5, 1.0 - (v.position.y * 0.5 + 0.5));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Shared helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn luminance(c: vec3<f32>) -> f32 {
|
||||||
|
return dot(c, vec3<f32>(0.2126, 0.7152, 0.0722));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Soft-knee bright-pass: returns the colour with sub-threshold values attenuated.
|
||||||
|
fn bright_extract(color: vec3<f32>, threshold: f32, soft: f32) -> vec3<f32> {
|
||||||
|
let lum = luminance(color);
|
||||||
|
let knee = threshold * soft;
|
||||||
|
// Quadratic soft knee
|
||||||
|
let ramp = clamp(lum - threshold + knee, 0.0, 2.0 * knee);
|
||||||
|
let weight = select(
|
||||||
|
select(0.0, 1.0, lum >= threshold),
|
||||||
|
ramp * ramp / (4.0 * knee + 0.00001),
|
||||||
|
lum >= threshold - knee && lum < threshold,
|
||||||
|
);
|
||||||
|
// When lum >= threshold we pass through, otherwise fade via weight
|
||||||
|
if lum >= threshold {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
return color * (weight / max(lum, 0.00001));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Downsample pass ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 5-sample box filter: centre × 4 + 4 diagonal neighbours × 1, divided by 8.
|
||||||
|
/// When bloom.threshold > 0 a bright-pass is applied after filtering.
|
||||||
|
@fragment
|
||||||
|
fn fs_downsample(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
let uv = in.uv;
|
||||||
|
let texel = 1.0 / vec2<f32>(textureDimensions(t_input));
|
||||||
|
|
||||||
|
let center = textureSample(t_input, s_input, uv).rgb;
|
||||||
|
let tl = textureSample(t_input, s_input, uv + vec2<f32>(-1.0, -1.0) * texel).rgb;
|
||||||
|
let tr = textureSample(t_input, s_input, uv + vec2<f32>( 1.0, -1.0) * texel).rgb;
|
||||||
|
let bl = textureSample(t_input, s_input, uv + vec2<f32>(-1.0, 1.0) * texel).rgb;
|
||||||
|
let br = textureSample(t_input, s_input, uv + vec2<f32>( 1.0, 1.0) * texel).rgb;
|
||||||
|
|
||||||
|
var color = (center * 4.0 + tl + tr + bl + br) / 8.0;
|
||||||
|
|
||||||
|
// Bright extraction on the first downsample level (threshold guard)
|
||||||
|
if bloom.threshold > 0.0 {
|
||||||
|
color = bright_extract(color, bloom.threshold, bloom.soft_threshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
return vec4<f32>(color, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Upsample pass ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 9-tap tent filter (3×3):
|
||||||
|
/// corners × 1, axis-aligned edges × 2, centre × 4 → /16
|
||||||
|
@fragment
|
||||||
|
fn fs_upsample(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
let uv = in.uv;
|
||||||
|
let texel = 1.0 / vec2<f32>(textureDimensions(t_input));
|
||||||
|
|
||||||
|
let tl = textureSample(t_input, s_input, uv + vec2<f32>(-1.0, -1.0) * texel).rgb;
|
||||||
|
let tc = textureSample(t_input, s_input, uv + vec2<f32>( 0.0, -1.0) * texel).rgb;
|
||||||
|
let tr = textureSample(t_input, s_input, uv + vec2<f32>( 1.0, -1.0) * texel).rgb;
|
||||||
|
let ml = textureSample(t_input, s_input, uv + vec2<f32>(-1.0, 0.0) * texel).rgb;
|
||||||
|
let mc = textureSample(t_input, s_input, uv).rgb;
|
||||||
|
let mr = textureSample(t_input, s_input, uv + vec2<f32>( 1.0, 0.0) * texel).rgb;
|
||||||
|
let bl = textureSample(t_input, s_input, uv + vec2<f32>(-1.0, 1.0) * texel).rgb;
|
||||||
|
let bc = textureSample(t_input, s_input, uv + vec2<f32>( 0.0, 1.0) * texel).rgb;
|
||||||
|
let br = textureSample(t_input, s_input, uv + vec2<f32>( 1.0, 1.0) * texel).rgb;
|
||||||
|
|
||||||
|
// Tent weights: corners×1 + edges×2 + centre×4 → sum=16
|
||||||
|
let color = (tl + tr + bl + br
|
||||||
|
+ (tc + ml + mr + bc) * 2.0
|
||||||
|
+ mc * 4.0) / 16.0;
|
||||||
|
|
||||||
|
return vec4<f32>(color, 1.0);
|
||||||
|
}
|
||||||
68
crates/voltex_renderer/src/tonemap_shader.wgsl
Normal file
68
crates/voltex_renderer/src/tonemap_shader.wgsl
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
// Tonemap pass shader.
|
||||||
|
// Combines HDR scene colour + bloom, applies exposure and ACES tonemapping,
|
||||||
|
// then converts to gamma-corrected sRGB for the swapchain.
|
||||||
|
|
||||||
|
// ── Group 0 ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@group(0) @binding(0) var t_hdr: texture_2d<f32>;
|
||||||
|
@group(0) @binding(1) var t_bloom: texture_2d<f32>;
|
||||||
|
@group(0) @binding(2) var s_sampler: sampler;
|
||||||
|
|
||||||
|
struct TonemapUniform {
|
||||||
|
bloom_intensity: f32,
|
||||||
|
exposure: f32,
|
||||||
|
_padding: vec2<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
@group(0) @binding(3) var<uniform> tonemap: TonemapUniform;
|
||||||
|
|
||||||
|
// ── Vertex stage ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
struct VertexInput {
|
||||||
|
@location(0) position: vec2<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) clip_position: vec4<f32>,
|
||||||
|
@location(0) uv: vec2<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_main(v: VertexInput) -> VertexOutput {
|
||||||
|
var out: VertexOutput;
|
||||||
|
out.clip_position = vec4<f32>(v.position, 0.0, 1.0);
|
||||||
|
out.uv = vec2<f32>(v.position.x * 0.5 + 0.5, 1.0 - (v.position.y * 0.5 + 0.5));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ACES tonemap ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn aces_tonemap(x: vec3<f32>) -> vec3<f32> {
|
||||||
|
let num = x * (2.51 * x + vec3<f32>(0.03));
|
||||||
|
let den = x * (2.43 * x + vec3<f32>(0.59)) + vec3<f32>(0.14);
|
||||||
|
return clamp(num / den, vec3<f32>(0.0), vec3<f32>(1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Fragment stage ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
let uv = in.uv;
|
||||||
|
|
||||||
|
let hdr = textureSample(t_hdr, s_sampler, uv).rgb;
|
||||||
|
let bloom = textureSample(t_bloom, s_sampler, uv).rgb;
|
||||||
|
|
||||||
|
// Combine HDR + bloom
|
||||||
|
var color = hdr + bloom * tonemap.bloom_intensity;
|
||||||
|
|
||||||
|
// Apply exposure
|
||||||
|
color = color * tonemap.exposure;
|
||||||
|
|
||||||
|
// ACES tonemapping
|
||||||
|
color = aces_tonemap(color);
|
||||||
|
|
||||||
|
// Gamma correction (linear → sRGB, γ = 2.2)
|
||||||
|
color = pow(color, vec3<f32>(1.0 / 2.2));
|
||||||
|
|
||||||
|
return vec4<f32>(color, 1.0);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user