Files
game_engine/crates/voltex_renderer/src/bloom.rs
tolelom 6c938999e4 feat(renderer): add HDR target, Bloom resources, and ACES tonemap
- Add hdr.rs with HdrTarget (Rgba16Float render target) and HDR_FORMAT constant
- Add bloom.rs with BloomResources (5-level mip chain), BloomUniform, and mip_sizes()
- Add tonemap.rs with TonemapUniform and CPU-side aces_tonemap() for testing
- Export all new types from lib.rs
- 33 tests passing (26 existing + 3 bloom + 4 tonemap)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 13:47:19 +09:00

146 lines
5.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use bytemuck::{Pod, Zeroable};
use crate::hdr::HDR_FORMAT;
/// Number of bloom mip levels (downsample + upsample chain).
pub const BLOOM_MIP_COUNT: usize = 5;
/// Uniform buffer for the bloom pass.
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
pub struct BloomUniform {
/// Luminance threshold above which a pixel contributes to bloom.
pub threshold: f32,
/// Soft knee for the threshold.
pub soft_threshold: f32,
pub _padding: [f32; 2],
}
impl Default for BloomUniform {
fn default() -> Self {
Self {
threshold: 1.0,
soft_threshold: 0.5,
_padding: [0.0; 2],
}
}
}
/// GPU resources for the bloom pass (mip chain + uniform buffer).
pub struct BloomResources {
/// One `TextureView` per mip level (5 levels).
pub mip_views: Vec<wgpu::TextureView>,
pub uniform_buffer: wgpu::Buffer,
/// Blend intensity applied during the tonemap pass.
pub intensity: f32,
}
impl BloomResources {
pub fn new(device: &wgpu::Device, width: u32, height: u32) -> Self {
let mip_views = create_mip_views(device, width, height);
let uniform = BloomUniform::default();
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Bloom Uniform Buffer"),
size: std::mem::size_of::<BloomUniform>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
// Initialize with defaults via a write at construction time would require a queue;
// callers may write the buffer themselves. The buffer is left zero-initialised here.
Self {
mip_views,
uniform_buffer,
intensity: 0.5,
}
}
/// Recreate the mip-chain textures when the window is resized.
pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
self.mip_views = create_mip_views(device, width, height);
}
}
// ── Public helpers ────────────────────────────────────────────────────────────
/// Return the (width, height) of each bloom mip level.
///
/// Level 0 = width/2 × height/2; each subsequent level halves again.
/// Sizes are clamped to a minimum of 1.
pub fn mip_sizes(width: u32, height: u32) -> Vec<(u32, u32)> {
let mut sizes = Vec::with_capacity(BLOOM_MIP_COUNT);
let mut w = (width / 2).max(1);
let mut h = (height / 2).max(1);
for _ in 0..BLOOM_MIP_COUNT {
sizes.push((w, h));
w = (w / 2).max(1);
h = (h / 2).max(1);
}
sizes
}
// ── Internal helpers ──────────────────────────────────────────────────────────
fn create_mip_views(device: &wgpu::Device, width: u32, height: u32) -> Vec<wgpu::TextureView> {
let sizes = mip_sizes(width, height);
sizes
.into_iter()
.enumerate()
.map(|(i, (w, h))| {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some(&format!("Bloom Mip {} Texture", i)),
size: wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: HDR_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
texture.create_view(&wgpu::TextureViewDescriptor::default())
})
.collect()
}
// ── Tests ─────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mip_sizes_1920_1080() {
let sizes = mip_sizes(1920, 1080);
assert_eq!(sizes.len(), BLOOM_MIP_COUNT);
assert_eq!(sizes[0], (960, 540));
assert_eq!(sizes[1], (480, 270));
assert_eq!(sizes[2], (240, 135));
assert_eq!(sizes[3], (120, 67));
assert_eq!(sizes[4], (60, 33));
}
#[test]
fn mip_sizes_64_64() {
let sizes = mip_sizes(64, 64);
assert_eq!(sizes.len(), BLOOM_MIP_COUNT);
assert_eq!(sizes[0], (32, 32));
assert_eq!(sizes[1], (16, 16));
assert_eq!(sizes[2], (8, 8));
assert_eq!(sizes[3], (4, 4));
assert_eq!(sizes[4], (2, 2));
}
#[test]
fn bloom_uniform_default() {
let u = BloomUniform::default();
assert!((u.threshold - 1.0).abs() < f32::EPSILON);
assert!((u.soft_threshold - 0.5).abs() < f32::EPSILON);
assert_eq!(u._padding, [0.0f32; 2]);
}
}