docs: add Phase 7-4 post processing status, spec, plan, and deferred items
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,14 @@
|
|||||||
- **raycast_all (다중 hit)** — 가장 가까운 hit만 반환.
|
- **raycast_all (다중 hit)** — 가장 가까운 hit만 반환.
|
||||||
- **BVH 조기 종료 최적화** — 모든 리프 검사 후 최소 t 선택. front-to-back 순회 미구현.
|
- **BVH 조기 종료 최적화** — 모든 리프 검사 후 최소 t 선택. front-to-back 순회 미구현.
|
||||||
|
|
||||||
|
## 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 미적용.
|
||||||
|
|
||||||
## Phase 7-3
|
## Phase 7-3
|
||||||
|
|
||||||
- **RT Reflections** — 미구현. BLAS/TLAS 인프라 재사용 가능.
|
- **RT Reflections** — 미구현. BLAS/TLAS 인프라 재사용 가능.
|
||||||
|
|||||||
@@ -113,6 +113,13 @@
|
|||||||
- voltex_renderer: Lighting pass RT shadow integration
|
- voltex_renderer: Lighting pass RT shadow integration
|
||||||
- deferred_demo updated with hardware RT shadows (4-pass: GBuffer → SSGI → RT Shadow → Lighting)
|
- deferred_demo updated with hardware RT shadows (4-pass: GBuffer → SSGI → RT Shadow → Lighting)
|
||||||
|
|
||||||
|
### 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 (7 passes)
|
||||||
|
|
||||||
## Crate 구조
|
## Crate 구조
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -126,7 +133,7 @@ crates/
|
|||||||
└── voltex_audio — AudioClip, WAV parser, mixing, WASAPI backend, AudioSystem, MixGroup, spatial
|
└── voltex_audio — AudioClip, WAV parser, mixing, WASAPI backend, AudioSystem, MixGroup, spatial
|
||||||
```
|
```
|
||||||
|
|
||||||
## 테스트: 206개 전부 통과
|
## 테스트: 213개 전부 통과
|
||||||
|
|
||||||
- voltex_asset: 14
|
- voltex_asset: 14
|
||||||
- voltex_audio: 35 (audio_clip 2 + wav 5 + mixing 11 + audio_system 2 + spatial 8 + mix_group 7)
|
- voltex_audio: 35 (audio_clip 2 + wav 5 + mixing 11 + audio_system 2 + spatial 8 + mix_group 7)
|
||||||
@@ -134,7 +141,7 @@ crates/
|
|||||||
- voltex_math: 37 (29 + AABB 6 + Ray 2)
|
- voltex_math: 37 (29 + AABB 6 + Ray 2)
|
||||||
- voltex_physics: 52 (collider 2 + narrow 11 + bvh 5 + collision 7 + rigid_body 3 + integrator 3 + solver 5 + ray 10 + raycast 6)
|
- voltex_physics: 52 (collider 2 + narrow 11 + bvh 5 + collision 7 + rigid_body 3 + integrator 3 + solver 5 + ray 10 + raycast 6)
|
||||||
- voltex_platform: 3
|
- voltex_platform: 3
|
||||||
- voltex_renderer: 26 (20 + SSGI 3 + RT 3)
|
- voltex_renderer: 33 (20 + SSGI 3 + RT 3 + bloom 3 + tonemap 4)
|
||||||
|
|
||||||
## Examples (11개)
|
## Examples (11개)
|
||||||
|
|
||||||
@@ -150,7 +157,7 @@ crates/
|
|||||||
- audio_demo — 사인파 오디오 재생
|
- audio_demo — 사인파 오디오 재생
|
||||||
- deferred_demo — 디퍼드 렌더링 + 다중 포인트 라이트
|
- deferred_demo — 디퍼드 렌더링 + 다중 포인트 라이트
|
||||||
|
|
||||||
## 다음: Phase 7-4 (포스트 프로세싱) — Stretch Goal
|
## 다음: Phase 8 (AI, 네트워킹, 스크립팅, 에디터) — Stretch Goal
|
||||||
|
|
||||||
스펙 참조: `docs/superpowers/specs/2026-03-24-voltex-engine-design.md`
|
스펙 참조: `docs/superpowers/specs/2026-03-24-voltex-engine-design.md`
|
||||||
|
|
||||||
|
|||||||
722
docs/superpowers/plans/2026-03-25-phase7-4-post-processing.md
Normal file
722
docs/superpowers/plans/2026-03-25-phase7-4-post-processing.md
Normal file
@@ -0,0 +1,722 @@
|
|||||||
|
# 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"
|
||||||
|
```
|
||||||
219
docs/superpowers/specs/2026-03-25-phase7-4-post-processing.md
Normal file
219
docs/superpowers/specs/2026-03-25-phase7-4-post-processing.md
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
# Phase 7-4: HDR + Bloom + ACES Tonemap — Design Spec
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
디퍼드 파이프라인을 HDR로 전환하고, Bloom과 ACES 톤매핑 포스트 프로세싱을 추가한다.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- HDR 렌더 타겟 (Rgba16Float)
|
||||||
|
- Lighting Pass HDR 출력 (Reinhard/감마 제거)
|
||||||
|
- Bloom (bright extract + downsample chain + upsample blend)
|
||||||
|
- ACES 톤매핑 + bloom 합성 + 감마 보정
|
||||||
|
- deferred_demo에 HDR+Bloom+Tonemap 통합
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- TAA (Temporal Anti-Aliasing)
|
||||||
|
- SSR (Screen-Space Reflections)
|
||||||
|
- Motion Blur, Depth of Field
|
||||||
|
- Adaptive exposure / auto-exposure
|
||||||
|
- Filmic 등 다른 톤매핑 커브
|
||||||
|
|
||||||
|
## Render Pass Flow (최종)
|
||||||
|
|
||||||
|
```
|
||||||
|
Pass 1: G-Buffer (변경 없음)
|
||||||
|
Pass 2: SSGI (변경 없음)
|
||||||
|
Pass 3: RT Shadow (변경 없음)
|
||||||
|
Pass 4: Lighting → HDR texture (Rgba16Float) — 톤매핑/감마 제거
|
||||||
|
Pass 5: Bloom (NEW) — bright extract → downsample chain → upsample chain
|
||||||
|
Pass 6: Tonemap (NEW) — HDR + Bloom → ACES tonemap + gamma → surface
|
||||||
|
```
|
||||||
|
|
||||||
|
## Module Structure
|
||||||
|
|
||||||
|
### 새 파일
|
||||||
|
- `crates/voltex_renderer/src/hdr.rs` — HdrTarget (Create)
|
||||||
|
- `crates/voltex_renderer/src/bloom.rs` — BloomResources, mip chain 생성 (Create)
|
||||||
|
- `crates/voltex_renderer/src/bloom_shader.wgsl` — downsample + 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)
|
||||||
|
|
||||||
|
## Types
|
||||||
|
|
||||||
|
### HdrTarget
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub const HDR_FORMAT: TextureFormat = TextureFormat::Rgba16Float;
|
||||||
|
|
||||||
|
pub struct HdrTarget {
|
||||||
|
pub view: TextureView,
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- `new(device, width, height)` — Rgba16Float, RENDER_ATTACHMENT | TEXTURE_BINDING
|
||||||
|
- `resize(device, width, height)`
|
||||||
|
|
||||||
|
### BloomResources
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct BloomResources {
|
||||||
|
pub mip_views: Vec<TextureView>, // 5 levels
|
||||||
|
pub threshold: f32,
|
||||||
|
pub intensity: f32,
|
||||||
|
pub bloom_uniform: Buffer,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mip chain**: 5단계, 각 레벨은 이전의 1/2 크기.
|
||||||
|
- Level 0: width/2 x height/2
|
||||||
|
- Level 1: width/4 x height/4
|
||||||
|
- Level 2: width/8 x height/8
|
||||||
|
- Level 3: width/16 x height/16
|
||||||
|
- Level 4: width/32 x height/32
|
||||||
|
|
||||||
|
각 mip은 Rgba16Float, RENDER_ATTACHMENT | TEXTURE_BINDING.
|
||||||
|
|
||||||
|
### BloomUniform
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct BloomUniform {
|
||||||
|
pub threshold: f32,
|
||||||
|
pub soft_threshold: f32, // threshold - knee
|
||||||
|
pub _padding: [f32; 2],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### TonemapUniform
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct TonemapUniform {
|
||||||
|
pub bloom_intensity: f32,
|
||||||
|
pub exposure: f32,
|
||||||
|
pub _padding: [f32; 2],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bloom Algorithm
|
||||||
|
|
||||||
|
### Pass 5a: Bright Extract + First Downsample
|
||||||
|
|
||||||
|
입력: HDR 텍스처 (full res)
|
||||||
|
출력: Bloom mip 0 (half res)
|
||||||
|
|
||||||
|
```wgsl
|
||||||
|
let color = textureSample(hdr, sampler, uv);
|
||||||
|
let luminance = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722));
|
||||||
|
let contribution = max(luminance - threshold, 0.0);
|
||||||
|
let bloom_color = color.rgb * (contribution / (luminance + 0.0001));
|
||||||
|
// 2x2 box filter (downsample)
|
||||||
|
output = bloom_color;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pass 5b: Downsample Chain (4회)
|
||||||
|
|
||||||
|
mip N → mip N+1: 텍스처 샘플링으로 자연스럽게 2x 다운샘플. 13-tap box filter로 품질 향상.
|
||||||
|
|
||||||
|
```wgsl
|
||||||
|
// 13-tap downsample (Karis average 스타일)
|
||||||
|
let a = textureSample(src, s, uv + vec2(-2, -2) * texel);
|
||||||
|
let b = textureSample(src, s, uv + vec2( 0, -2) * texel);
|
||||||
|
// ... (center + 4 corners + 4 edges + 4 diagonal)
|
||||||
|
output = weighted_average;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pass 5c: Upsample Chain (4회)
|
||||||
|
|
||||||
|
mip N+1 → mip N: bilinear 업샘플 + additive blend with current level.
|
||||||
|
|
||||||
|
```wgsl
|
||||||
|
let upsampled = textureSample(lower_mip, s, uv);
|
||||||
|
let current = textureSample(current_mip, s, uv);
|
||||||
|
output = current + upsampled;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 최종 bloom = mip 0 (half res)
|
||||||
|
|
||||||
|
## Tonemap Pass (Pass 6)
|
||||||
|
|
||||||
|
입력: HDR 텍스처 + Bloom 텍스처 (mip 0)
|
||||||
|
출력: surface texture (sRGB)
|
||||||
|
|
||||||
|
```wgsl
|
||||||
|
let hdr_color = textureSample(t_hdr, s, uv).rgb;
|
||||||
|
let bloom_color = textureSample(t_bloom, s, uv).rgb;
|
||||||
|
let combined = hdr_color + bloom_color * bloom_intensity;
|
||||||
|
let exposed = combined * exposure;
|
||||||
|
let tonemapped = aces_tonemap(exposed);
|
||||||
|
let gamma = pow(tonemapped, vec3(1.0 / 2.2));
|
||||||
|
output = vec4(gamma, 1.0);
|
||||||
|
```
|
||||||
|
|
||||||
|
### ACES Filmic Tonemap
|
||||||
|
|
||||||
|
```wgsl
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lighting Pass 수정
|
||||||
|
|
||||||
|
현재 `deferred_lighting.wgsl` 끝부분:
|
||||||
|
```wgsl
|
||||||
|
// Reinhard tone mapping
|
||||||
|
color = color / (color + vec3<f32>(1.0));
|
||||||
|
// Gamma correction
|
||||||
|
color = pow(color, vec3<f32>(1.0 / 2.2));
|
||||||
|
```
|
||||||
|
|
||||||
|
변경: 이 두 줄을 **제거**. raw HDR 값을 그대로 출력.
|
||||||
|
|
||||||
|
Lighting pipeline의 color target format을 surface format → HDR_FORMAT(Rgba16Float)으로 변경.
|
||||||
|
|
||||||
|
## Bind Group Details
|
||||||
|
|
||||||
|
### Bloom Downsample/Upsample (각 패스)
|
||||||
|
- Group 0: 입력 텍스처 + sampler + BloomUniform (downsample 시)
|
||||||
|
단일 파이프라인을 재사용, 입력/출력만 변경.
|
||||||
|
|
||||||
|
### Tonemap
|
||||||
|
- Group 0: HDR 텍스처 + bloom 텍스처 + sampler + TonemapUniform
|
||||||
|
|
||||||
|
## Simplified Bloom (구현 단순화)
|
||||||
|
|
||||||
|
복잡한 13-tap 대신, 첫 구현은:
|
||||||
|
1. Bright extract → mip 0 (half res, single fullscreen pass)
|
||||||
|
2. Downsample: 순차 4회 (bilinear sampler가 자동 2x2 평균)
|
||||||
|
3. Upsample: 순차 4회 (additive blend)
|
||||||
|
4. 총 9 fullscreen passes (1 extract + 4 down + 4 up)
|
||||||
|
|
||||||
|
단일 셰이더 + 다른 바인드 그룹으로 반복 실행.
|
||||||
|
|
||||||
|
## Test Plan
|
||||||
|
|
||||||
|
### bloom.rs
|
||||||
|
- mip_sizes: 입력 크기에서 5단계 크기 계산
|
||||||
|
- BloomResources 생성
|
||||||
|
|
||||||
|
### tonemap.rs
|
||||||
|
- ACES tonemap: 0→0, 1→~0.59, large→~1.0
|
||||||
|
|
||||||
|
### 통합 (수동)
|
||||||
|
- deferred_demo: bloom ON/OFF 비교 (밝은 라이트 주변 glow)
|
||||||
|
- exposure 조절 (어두움/밝음)
|
||||||
Reference in New Issue
Block a user