Files
game_engine/docs/superpowers/plans/2026-03-25-phase7-4-post-processing.md
2026-03-25 13:55:09 +09:00

723 lines
24 KiB
Markdown

# Phase 7-4: HDR + Bloom + ACES Tonemap Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** HDR 파이프라인으로 전환하고 Bloom + ACES 톤매핑으로 AAA 비주얼 품질 달성
**Architecture:** Lighting Pass 출력을 Rgba16Float HDR 텍스처로 변경. Bloom 패스(bright extract + 5-level downsample/upsample chain). Tonemap 패스(HDR + Bloom → ACES + 감마 → surface). 총 9 bloom sub-passes + 1 tonemap pass 추가.
**Tech Stack:** Rust, wgpu 28.0, WGSL
**Spec:** `docs/superpowers/specs/2026-03-25-phase7-4-post-processing.md`
---
## File Structure
### 새 파일
- `crates/voltex_renderer/src/hdr.rs` — HdrTarget (Create)
- `crates/voltex_renderer/src/bloom.rs` — BloomResources, BloomUniform, mip chain (Create)
- `crates/voltex_renderer/src/bloom_shader.wgsl` — downsample (bright extract) + upsample (Create)
- `crates/voltex_renderer/src/tonemap.rs` — TonemapUniform (Create)
- `crates/voltex_renderer/src/tonemap_shader.wgsl` — ACES + bloom merge + gamma (Create)
### 수정 파일
- `crates/voltex_renderer/src/deferred_pipeline.rs` — bloom/tonemap 파이프라인 추가 (Modify)
- `crates/voltex_renderer/src/deferred_lighting.wgsl` — Reinhard+감마 제거 (Modify)
- `crates/voltex_renderer/src/lib.rs` — 새 모듈 등록 (Modify)
- `examples/deferred_demo/src/main.rs` — HDR+Bloom+Tonemap 통합 (Modify)
---
## Task 1: HdrTarget + Bloom/Tonemap 리소스
**Files:**
- Create: `crates/voltex_renderer/src/hdr.rs`
- Create: `crates/voltex_renderer/src/bloom.rs`
- Create: `crates/voltex_renderer/src/tonemap.rs`
- Modify: `crates/voltex_renderer/src/lib.rs`
- [ ] **Step 1: hdr.rs 작성**
```rust
// crates/voltex_renderer/src/hdr.rs
pub const HDR_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
pub struct HdrTarget {
pub view: wgpu::TextureView,
pub width: u32,
pub height: u32,
}
impl HdrTarget {
pub fn new(device: &wgpu::Device, width: u32, height: u32) -> Self {
let view = create_hdr_texture(device, width, height);
Self { view, width, height }
}
pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
self.view = create_hdr_texture(device, width, height);
self.width = width;
self.height = height;
}
}
fn create_hdr_texture(device: &wgpu::Device, w: u32, h: u32) -> wgpu::TextureView {
let tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("HDR Target"),
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: &[],
});
tex.create_view(&wgpu::TextureViewDescriptor::default())
}
```
- [ ] **Step 2: bloom.rs 작성**
```rust
// crates/voltex_renderer/src/bloom.rs
use bytemuck::{Pod, Zeroable};
use wgpu::util::DeviceExt;
pub const BLOOM_MIP_COUNT: usize = 5;
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
pub struct BloomUniform {
pub threshold: f32,
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],
}
}
}
pub struct BloomResources {
pub mip_views: Vec<wgpu::TextureView>,
pub uniform_buffer: wgpu::Buffer,
pub intensity: f32,
}
impl BloomResources {
pub fn new(device: &wgpu::Device, width: u32, height: u32) -> Self {
let mip_views = create_mip_chain(device, width, height);
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Bloom Uniform"),
contents: bytemuck::bytes_of(&BloomUniform::default()),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
Self { mip_views, uniform_buffer, intensity: 0.5 }
}
pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
self.mip_views = create_mip_chain(device, width, height);
}
}
fn create_mip_chain(device: &wgpu::Device, width: u32, height: u32) -> Vec<wgpu::TextureView> {
let sizes = mip_sizes(width, height);
sizes.iter().enumerate().map(|(i, (w, h))| {
let tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some(&format!("Bloom Mip {}", 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: crate::hdr::HDR_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
tex.create_view(&wgpu::TextureViewDescriptor::default())
}).collect()
}
/// Calculate mip chain sizes (5 levels, each half of previous).
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
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mip_sizes() {
let sizes = mip_sizes(1920, 1080);
assert_eq!(sizes.len(), 5);
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 test_mip_sizes_small() {
let sizes = mip_sizes(64, 64);
assert_eq!(sizes[0], (32, 32));
assert_eq!(sizes[4], (2, 2));
}
#[test]
fn test_bloom_uniform_default() {
let u = BloomUniform::default();
assert!((u.threshold - 1.0).abs() < 1e-5);
}
}
```
- [ ] **Step 3: tonemap.rs 작성**
```rust
// crates/voltex_renderer/src/tonemap.rs
use bytemuck::{Pod, Zeroable};
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
pub struct TonemapUniform {
pub bloom_intensity: f32,
pub exposure: f32,
pub _padding: [f32; 2],
}
impl Default for TonemapUniform {
fn default() -> Self {
Self {
bloom_intensity: 0.5,
exposure: 1.0,
_padding: [0.0; 2],
}
}
}
/// ACES tonemap (CPU version for testing).
pub fn aces_tonemap(x: f32) -> f32 {
let a = 2.51;
let b = 0.03;
let c = 2.43;
let d = 0.59;
let e = 0.14;
((x * (a * x + b)) / (x * (c * x + d) + e)).clamp(0.0, 1.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_aces_zero() {
assert!((aces_tonemap(0.0)).abs() < 1e-5);
}
#[test]
fn test_aces_one() {
let v = aces_tonemap(1.0);
assert!(v > 0.5 && v < 0.7, "aces(1.0) = {}, expected ~0.59", v);
}
#[test]
fn test_aces_high() {
let v = aces_tonemap(10.0);
assert!(v > 0.95, "aces(10.0) = {}, expected ~1.0", v);
}
#[test]
fn test_tonemap_uniform_default() {
let u = TonemapUniform::default();
assert!((u.exposure - 1.0).abs() < 1e-5);
assert!((u.bloom_intensity - 0.5).abs() < 1e-5);
}
}
```
- [ ] **Step 4: lib.rs에 모듈 등록**
```rust
pub mod hdr;
pub mod bloom;
pub mod tonemap;
pub use hdr::{HdrTarget, HDR_FORMAT};
pub use bloom::{BloomResources, BloomUniform, mip_sizes, BLOOM_MIP_COUNT};
pub use tonemap::{TonemapUniform, aces_tonemap};
```
- [ ] **Step 5: 빌드 + 테스트**
Run: `cargo test -p voltex_renderer`
Expected: 기존 26 + 7 = 33 PASS
- [ ] **Step 6: 커밋**
```bash
git add crates/voltex_renderer/src/hdr.rs crates/voltex_renderer/src/bloom.rs crates/voltex_renderer/src/tonemap.rs crates/voltex_renderer/src/lib.rs
git commit -m "feat(renderer): add HDR target, Bloom resources, and ACES tonemap"
```
---
## Task 2: Bloom + Tonemap 셰이더
**Files:**
- Create: `crates/voltex_renderer/src/bloom_shader.wgsl`
- Create: `crates/voltex_renderer/src/tonemap_shader.wgsl`
- [ ] **Step 1: bloom_shader.wgsl 작성**
Single shader with two entry points: downsample (bright extract on first pass) and upsample.
```wgsl
// Bloom shader: downsample with bright extract + upsample with additive blend
struct BloomUniform {
threshold: f32,
soft_threshold: f32,
};
@group(0) @binding(0) var t_input: texture_2d<f32>;
@group(0) @binding(1) var s_input: sampler;
@group(0) @binding(2) var<uniform> bloom: BloomUniform;
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) uv: vec2<f32>,
};
@vertex
fn vs_main(@location(0) position: vec2<f32>) -> VertexOutput {
var out: VertexOutput;
out.clip_position = vec4<f32>(position, 0.0, 1.0);
out.uv = vec2<f32>(position.x * 0.5 + 0.5, 1.0 - (position.y * 0.5 + 0.5));
return out;
}
// Downsample with optional bright extract (first pass uses threshold, subsequent passes threshold=0)
@fragment
fn fs_downsample(in: VertexOutput) -> @location(0) vec4<f32> {
let texel_size = 1.0 / vec2<f32>(textureDimensions(t_input));
// 4-sample box filter with offset for better quality
let a = textureSample(t_input, s_input, in.uv + texel_size * vec2(-1.0, -1.0)).rgb;
let b = textureSample(t_input, s_input, in.uv + texel_size * vec2( 1.0, -1.0)).rgb;
let c = textureSample(t_input, s_input, in.uv + texel_size * vec2(-1.0, 1.0)).rgb;
let d = textureSample(t_input, s_input, in.uv + texel_size * vec2( 1.0, 1.0)).rgb;
let center = textureSample(t_input, s_input, in.uv).rgb;
var color = (a + b + c + d + center * 4.0) / 8.0;
// Bright extract (only effective when threshold > 0)
if bloom.threshold > 0.0 {
let luminance = dot(color, vec3(0.2126, 0.7152, 0.0722));
let contribution = max(luminance - bloom.soft_threshold, 0.0) / max(luminance, 0.0001);
color = color * contribution;
}
return vec4(color, 1.0);
}
// Upsample with bilinear + additive blend (reads from lower mip)
@fragment
fn fs_upsample(in: VertexOutput) -> @location(0) vec4<f32> {
let texel_size = 1.0 / vec2<f32>(textureDimensions(t_input));
// 9-tap tent filter for smooth upsample
var color = textureSample(t_input, s_input, in.uv).rgb * 4.0;
color += textureSample(t_input, s_input, in.uv + vec2( texel_size.x, 0.0)).rgb * 2.0;
color += textureSample(t_input, s_input, in.uv + vec2(-texel_size.x, 0.0)).rgb * 2.0;
color += textureSample(t_input, s_input, in.uv + vec2(0.0, texel_size.y)).rgb * 2.0;
color += textureSample(t_input, s_input, in.uv + vec2(0.0, -texel_size.y)).rgb * 2.0;
color += textureSample(t_input, s_input, in.uv + vec2( texel_size.x, texel_size.y)).rgb;
color += textureSample(t_input, s_input, in.uv + vec2(-texel_size.x, texel_size.y)).rgb;
color += textureSample(t_input, s_input, in.uv + vec2( texel_size.x, -texel_size.y)).rgb;
color += textureSample(t_input, s_input, in.uv + vec2(-texel_size.x, -texel_size.y)).rgb;
color /= 16.0;
return vec4(color, 1.0);
}
```
- [ ] **Step 2: tonemap_shader.wgsl 작성**
```wgsl
// Tonemap pass: combine HDR + Bloom, apply ACES tonemap + gamma
struct TonemapUniform {
bloom_intensity: f32,
exposure: f32,
};
@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;
@group(0) @binding(3) var<uniform> tonemap: TonemapUniform;
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) uv: vec2<f32>,
};
@vertex
fn vs_main(@location(0) position: vec2<f32>) -> VertexOutput {
var out: VertexOutput;
out.clip_position = vec4<f32>(position, 0.0, 1.0);
out.uv = vec2<f32>(position.x * 0.5 + 0.5, 1.0 - (position.y * 0.5 + 0.5));
return out;
}
fn aces_tonemap(x: vec3<f32>) -> vec3<f32> {
let a = 2.51;
let b = 0.03;
let c = 2.43;
let d = 0.59;
let e = 0.14;
return clamp((x * (a * x + b)) / (x * (c * x + d) + e), vec3(0.0), vec3(1.0));
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let hdr_color = textureSample(t_hdr, s_sampler, in.uv).rgb;
let bloom_color = textureSample(t_bloom, s_sampler, in.uv).rgb;
// Combine HDR + Bloom
let combined = hdr_color + bloom_color * tonemap.bloom_intensity;
// Apply exposure
let exposed = combined * tonemap.exposure;
// ACES tonemap
let tonemapped = aces_tonemap(exposed);
// Gamma correction (linear → sRGB)
let gamma = pow(tonemapped, vec3(1.0 / 2.2));
return vec4(gamma, 1.0);
}
```
- [ ] **Step 3: 커밋**
```bash
git add crates/voltex_renderer/src/bloom_shader.wgsl crates/voltex_renderer/src/tonemap_shader.wgsl
git commit -m "feat(renderer): add bloom and tonemap shaders"
```
---
## Task 3: Bloom + Tonemap 파이프라인 + Lighting HDR 전환
**Files:**
- Modify: `crates/voltex_renderer/src/deferred_pipeline.rs`
- Modify: `crates/voltex_renderer/src/deferred_lighting.wgsl`
- [ ] **Step 1: deferred_pipeline.rs에 bloom/tonemap 파이프라인 함수 추가**
```rust
use crate::hdr::HDR_FORMAT;
use crate::fullscreen_quad::FullscreenVertex;
/// Bloom pass bind group layout (shared for downsample and upsample).
/// Group 0: input texture + sampler + BloomUniform
pub fn bloom_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Bloom BGL"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
})
}
/// Create bloom downsample pipeline (entry point: fs_downsample).
pub fn create_bloom_downsample_pipeline(
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
) -> wgpu::RenderPipeline {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Bloom Shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("bloom_shader.wgsl").into()),
});
let pipe_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Bloom Downsample Layout"),
bind_group_layouts: &[layout],
immediate_size: 0,
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Bloom Downsample"),
layout: Some(&pipe_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[FullscreenVertex::LAYOUT],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_downsample"),
targets: &[Some(wgpu::ColorTargetState {
format: HDR_FORMAT,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, ..Default::default() },
depth_stencil: None,
multisample: Default::default(),
multiview_mask: None,
cache: None,
})
}
/// Create bloom upsample pipeline (entry point: fs_upsample).
/// Output blends additively with the render target.
pub fn create_bloom_upsample_pipeline(
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
) -> wgpu::RenderPipeline {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Bloom Shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("bloom_shader.wgsl").into()),
});
let pipe_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Bloom Upsample Layout"),
bind_group_layouts: &[layout],
immediate_size: 0,
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Bloom Upsample"),
layout: Some(&pipe_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[FullscreenVertex::LAYOUT],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_upsample"),
targets: &[Some(wgpu::ColorTargetState {
format: HDR_FORMAT,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent::REPLACE,
}),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, ..Default::default() },
depth_stencil: None,
multisample: Default::default(),
multiview_mask: None,
cache: None,
})
}
/// Tonemap pass bind group layout.
pub fn tonemap_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Tonemap BGL"),
entries: &[
wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: true }, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false }, count: None },
wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: true }, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false }, count: None },
wgpu::BindGroupLayoutEntry { binding: 2, visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None },
wgpu::BindGroupLayoutEntry { binding: 3, visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None }, count: None },
],
})
}
/// Create tonemap pipeline.
pub fn create_tonemap_pipeline(
device: &wgpu::Device,
surface_format: wgpu::TextureFormat,
layout: &wgpu::BindGroupLayout,
) -> wgpu::RenderPipeline {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Tonemap Shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("tonemap_shader.wgsl").into()),
});
let pipe_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Tonemap Layout"),
bind_group_layouts: &[layout],
immediate_size: 0,
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Tonemap Pipeline"),
layout: Some(&pipe_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[FullscreenVertex::LAYOUT],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: surface_format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, ..Default::default() },
depth_stencil: None,
multisample: Default::default(),
multiview_mask: None,
cache: None,
})
}
```
- [ ] **Step 2: Lighting pipeline의 target format을 HDR로 변경**
In `create_lighting_pipeline`, change the `surface_format` parameter usage: the function should accept `HDR_FORMAT` instead for the color target. The caller passes HDR_FORMAT.
Actually, the function already takes `surface_format` as parameter. The caller (deferred_demo) will just pass `HDR_FORMAT` instead of the surface format.
- [ ] **Step 3: deferred_lighting.wgsl에서 Reinhard + 감마 제거**
Remove the last 4 lines before `return`:
```wgsl
// REMOVE these lines:
// color = color / (color + vec3<f32>(1.0)); // Reinhard
// color = pow(color, vec3<f32>(1.0 / 2.2)); // Gamma
```
The output is now raw HDR linear color.
- [ ] **Step 4: 빌드 확인**
Run: `cargo build -p voltex_renderer`
Expected: 컴파일 성공
- [ ] **Step 5: 커밋**
```bash
git add crates/voltex_renderer/src/deferred_pipeline.rs crates/voltex_renderer/src/deferred_lighting.wgsl
git commit -m "feat(renderer): add bloom/tonemap pipelines and convert lighting to HDR output"
```
---
## Task 4: deferred_demo에 HDR + Bloom + Tonemap 통합
**Files:**
- Modify: `examples/deferred_demo/src/main.rs`
이 태스크는 opus 모델로 실행.
변경사항:
1. HdrTarget 생성 (full resolution)
2. BloomResources 생성
3. Bloom 파이프라인 2개 (downsample + upsample) + bind group layout
4. Tonemap 파이프라인 + bind group layout + TonemapUniform buffer
5. Lighting Pass를 HDR texture에 렌더 (surface 대신)
6. Bloom Pass: 9 sub-passes (1 bright extract downsample + 4 downsample + 4 upsample)
7. Tonemap Pass: HDR + bloom mip[0] → ACES + gamma → surface
8. 리사이즈 시 HDR + Bloom 재생성
9. Bloom/Tonemap bind groups 생성 (각 sub-pass마다 다른 입출력)
- [ ] **Step 1: deferred_demo 수정**
- [ ] **Step 2: 빌드 확인**
Run: `cargo build --bin deferred_demo`
- [ ] **Step 3: 커밋**
```bash
git add examples/deferred_demo/src/main.rs
git commit -m "feat(renderer): add HDR + Bloom + ACES tonemap to deferred_demo"
```
---
## Task 5: 문서 업데이트
**Files:**
- Modify: `docs/STATUS.md`
- Modify: `docs/DEFERRED.md`
- [ ] **Step 1: STATUS.md에 Phase 7-4 추가**
```markdown
### Phase 7-4: Post Processing (HDR + Bloom + ACES)
- voltex_renderer: HdrTarget (Rgba16Float), Lighting → HDR output
- voltex_renderer: BloomResources (5-level mip chain, downsample/upsample)
- voltex_renderer: Bloom shader (bright extract + tent filter upsample)
- voltex_renderer: Tonemap shader (ACES filmic + bloom merge + gamma)
- deferred_demo updated with full post-processing pipeline (6 passes)
```
- [ ] **Step 2: DEFERRED.md에 Phase 7-4 미뤄진 항목**
```markdown
## Phase 7-4
- **TAA** — Temporal Anti-Aliasing 미구현. Motion vector 필요.
- **SSR** — Screen-Space Reflections 미구현.
- **Motion Blur, DOF** — 미구현.
- **Auto Exposure** — 고정 exposure만. 적응형 노출 미구현.
- **Bilateral Bloom Blur** — 단순 box/tent filter. Kawase blur 미적용.
```
- [ ] **Step 3: 커밋**
```bash
git add docs/STATUS.md docs/DEFERRED.md
git commit -m "docs: add Phase 7-4 post processing status and deferred items"
```