Skip to main content

proof_engine/editor/
post_fx.rs

1
2//! Post-processing stack editor — effects chain, parameter panels, preview modes.
3
4use glam::{Vec2, Vec3, Vec4};
5use std::collections::HashMap;
6
7// ---------------------------------------------------------------------------
8// Effect descriptors
9// ---------------------------------------------------------------------------
10
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum PostFxKind {
13    // Tone / exposure
14    Exposure,
15    ToneMapping,
16    ColorGrade,
17    LiftGammaGain,
18    HueSaturation,
19    ColorBalance,
20    Curves,
21    // Bloom / glow
22    Bloom,
23    LensFlare,
24    LightShafts,
25    // Depth of field
26    DepthOfField,
27    TiltShift,
28    // Motion
29    MotionBlur,
30    TemporalAA,
31    // Ambient
32    Ssao,
33    Hbao,
34    ScreenSpaceReflections,
35    // Vignette / film
36    Vignette,
37    FilmGrain,
38    ChromaticAberration,
39    LensDistortion,
40    Scanlines,
41    // Stylistic
42    Pixelate,
43    Posterize,
44    Crosshatch,
45    Halftone,
46    Outline,
47    // Blur
48    GaussianBlur,
49    RadialBlur,
50    DirectionalBlur,
51    // Color filters
52    Sepia,
53    Grayscale,
54    Infrared,
55    NightVision,
56    ThermalVision,
57    // Anti-aliasing
58    Fxaa,
59    Smaa,
60    Taa,
61    // HDR
62    AutoExposure,
63    HistogramEq,
64    // Water / weather
65    Underwater,
66    RainDrops,
67    Fog,
68    HeatHaze,
69    // Sharp
70    Sharpen,
71    UnsharpMask,
72    EdgeEnhance,
73    // Overlay
74    Scanlines2,
75    Crt,
76    VhsGlitch,
77    DigitalGlitch,
78}
79
80impl PostFxKind {
81    pub fn label(&self) -> &'static str {
82        match self {
83            PostFxKind::Exposure => "Exposure",
84            PostFxKind::ToneMapping => "Tone Mapping",
85            PostFxKind::ColorGrade => "Color Grade",
86            PostFxKind::LiftGammaGain => "Lift / Gamma / Gain",
87            PostFxKind::HueSaturation => "Hue / Saturation",
88            PostFxKind::ColorBalance => "Color Balance",
89            PostFxKind::Curves => "Curves",
90            PostFxKind::Bloom => "Bloom",
91            PostFxKind::LensFlare => "Lens Flare",
92            PostFxKind::LightShafts => "Light Shafts",
93            PostFxKind::DepthOfField => "Depth of Field",
94            PostFxKind::TiltShift => "Tilt Shift",
95            PostFxKind::MotionBlur => "Motion Blur",
96            PostFxKind::TemporalAA => "Temporal AA",
97            PostFxKind::Ssao => "SSAO",
98            PostFxKind::Hbao => "HBAO",
99            PostFxKind::ScreenSpaceReflections => "Screen-Space Reflections",
100            PostFxKind::Vignette => "Vignette",
101            PostFxKind::FilmGrain => "Film Grain",
102            PostFxKind::ChromaticAberration => "Chromatic Aberration",
103            PostFxKind::LensDistortion => "Lens Distortion",
104            PostFxKind::Scanlines | PostFxKind::Scanlines2 => "Scanlines",
105            PostFxKind::Pixelate => "Pixelate",
106            PostFxKind::Posterize => "Posterize",
107            PostFxKind::Crosshatch => "Crosshatch",
108            PostFxKind::Halftone => "Halftone",
109            PostFxKind::Outline => "Outline",
110            PostFxKind::GaussianBlur => "Gaussian Blur",
111            PostFxKind::RadialBlur => "Radial Blur",
112            PostFxKind::DirectionalBlur => "Directional Blur",
113            PostFxKind::Sepia => "Sepia",
114            PostFxKind::Grayscale => "Grayscale",
115            PostFxKind::Infrared => "Infrared",
116            PostFxKind::NightVision => "Night Vision",
117            PostFxKind::ThermalVision => "Thermal Vision",
118            PostFxKind::Fxaa => "FXAA",
119            PostFxKind::Smaa => "SMAA",
120            PostFxKind::Taa => "TAA",
121            PostFxKind::AutoExposure => "Auto Exposure",
122            PostFxKind::HistogramEq => "Histogram Equalization",
123            PostFxKind::Underwater => "Underwater",
124            PostFxKind::RainDrops => "Rain Drops",
125            PostFxKind::Fog => "Fog",
126            PostFxKind::HeatHaze => "Heat Haze",
127            PostFxKind::Sharpen => "Sharpen",
128            PostFxKind::UnsharpMask => "Unsharp Mask",
129            PostFxKind::EdgeEnhance => "Edge Enhance",
130            PostFxKind::Crt => "CRT",
131            PostFxKind::VhsGlitch => "VHS Glitch",
132            PostFxKind::DigitalGlitch => "Digital Glitch",
133        }
134    }
135
136    pub fn category(&self) -> &'static str {
137        match self {
138            PostFxKind::Exposure | PostFxKind::ToneMapping | PostFxKind::ColorGrade
139            | PostFxKind::LiftGammaGain | PostFxKind::HueSaturation | PostFxKind::ColorBalance
140            | PostFxKind::Curves => "Color",
141            PostFxKind::Bloom | PostFxKind::LensFlare | PostFxKind::LightShafts => "Glow",
142            PostFxKind::DepthOfField | PostFxKind::TiltShift => "Depth of Field",
143            PostFxKind::MotionBlur | PostFxKind::TemporalAA => "Motion",
144            PostFxKind::Ssao | PostFxKind::Hbao | PostFxKind::ScreenSpaceReflections => "Ambient",
145            PostFxKind::Vignette | PostFxKind::FilmGrain | PostFxKind::ChromaticAberration
146            | PostFxKind::LensDistortion => "Film",
147            PostFxKind::Pixelate | PostFxKind::Posterize | PostFxKind::Crosshatch
148            | PostFxKind::Halftone | PostFxKind::Outline => "Stylistic",
149            PostFxKind::GaussianBlur | PostFxKind::RadialBlur | PostFxKind::DirectionalBlur => "Blur",
150            PostFxKind::Sepia | PostFxKind::Grayscale | PostFxKind::Infrared
151            | PostFxKind::NightVision | PostFxKind::ThermalVision => "Filter",
152            PostFxKind::Fxaa | PostFxKind::Smaa | PostFxKind::Taa => "Anti-Aliasing",
153            PostFxKind::AutoExposure | PostFxKind::HistogramEq => "HDR",
154            PostFxKind::Underwater | PostFxKind::RainDrops | PostFxKind::Fog
155            | PostFxKind::HeatHaze => "Weather",
156            PostFxKind::Sharpen | PostFxKind::UnsharpMask | PostFxKind::EdgeEnhance => "Sharpen",
157            PostFxKind::Scanlines | PostFxKind::Scanlines2 | PostFxKind::Crt
158            | PostFxKind::VhsGlitch | PostFxKind::DigitalGlitch => "Retro",
159        }
160    }
161
162    pub fn default_params(&self) -> Vec<PostFxParam> {
163        match self {
164            PostFxKind::Exposure => vec![
165                PostFxParam::float("exposure", 0.0, -10.0, 10.0),
166            ],
167            PostFxKind::ToneMapping => vec![
168                PostFxParam::choice("mode", 1, &["Linear", "ACES", "Filmic", "Reinhard", "Uncharted2"]),
169                PostFxParam::float("white_point", 11.2, 1.0, 20.0),
170            ],
171            PostFxKind::Bloom => vec![
172                PostFxParam::float("threshold", 1.0, 0.0, 10.0),
173                PostFxParam::float("intensity", 0.5, 0.0, 10.0),
174                PostFxParam::float("scatter", 0.7, 0.0, 1.0),
175                PostFxParam::float("clamp", 65472.0, 0.0, 65472.0),
176                PostFxParam::color("tint", Vec4::ONE),
177            ],
178            PostFxKind::DepthOfField => vec![
179                PostFxParam::float("focus_distance", 10.0, 0.1, 1000.0),
180                PostFxParam::float("focal_length", 50.0, 1.0, 300.0),
181                PostFxParam::float("aperture", 5.6, 0.7, 32.0),
182                PostFxParam::float("blade_count", 5.0, 3.0, 11.0),
183                PostFxParam::float("blade_curvature", 1.0, 0.0, 1.0),
184                PostFxParam::toggle("near_blur", true),
185            ],
186            PostFxKind::Ssao => vec![
187                PostFxParam::float("radius", 0.5, 0.01, 5.0),
188                PostFxParam::float("intensity", 1.0, 0.0, 4.0),
189                PostFxParam::float("power", 1.0, 0.5, 4.0),
190                PostFxParam::int("samples", 16, 4, 64),
191                PostFxParam::float("bias", 0.025, 0.0, 0.2),
192            ],
193            PostFxKind::Vignette => vec![
194                PostFxParam::float("intensity", 0.4, 0.0, 1.0),
195                PostFxParam::float("smoothness", 0.4, 0.01, 1.0),
196                PostFxParam::float("rounded", 0.0, 0.0, 1.0),
197                PostFxParam::color("color", Vec4::new(0.0, 0.0, 0.0, 1.0)),
198                PostFxParam::vec2("center", Vec2::new(0.5, 0.5)),
199            ],
200            PostFxKind::FilmGrain => vec![
201                PostFxParam::float("intensity", 0.1, 0.0, 1.0),
202                PostFxParam::float("response", 0.8, 0.0, 1.0),
203                PostFxParam::toggle("colored", false),
204            ],
205            PostFxKind::ChromaticAberration => vec![
206                PostFxParam::float("intensity", 0.1, 0.0, 1.0),
207                PostFxParam::toggle("fast_mode", true),
208            ],
209            PostFxKind::MotionBlur => vec![
210                PostFxParam::float("shutter_angle", 270.0, 0.0, 360.0),
211                PostFxParam::int("sample_count", 8, 4, 32),
212                PostFxParam::float("quality", 1.0, 0.25, 2.0),
213            ],
214            PostFxKind::ScreenSpaceReflections => vec![
215                PostFxParam::float("max_march_distance", 100.0, 1.0, 500.0),
216                PostFxParam::int("max_iterations", 64, 8, 256),
217                PostFxParam::float("thickness", 0.5, 0.01, 10.0),
218                PostFxParam::float("fade_distance", 0.5, 0.0, 1.0),
219            ],
220            PostFxKind::Fog => vec![
221                PostFxParam::float("density", 0.05, 0.0, 1.0),
222                PostFxParam::float("start_distance", 0.0, 0.0, 1000.0),
223                PostFxParam::float("end_distance", 300.0, 0.0, 10000.0),
224                PostFxParam::color("color", Vec4::new(0.8, 0.9, 1.0, 1.0)),
225                PostFxParam::choice("mode", 0, &["Linear", "Exponential", "Exponential Squared"]),
226            ],
227            _ => vec![],
228        }
229    }
230}
231
232// ---------------------------------------------------------------------------
233// Parameter types
234// ---------------------------------------------------------------------------
235
236#[derive(Debug, Clone)]
237pub enum PostFxParamValue {
238    Float(f32),
239    Int(i32),
240    Bool(bool),
241    Vec2(Vec2),
242    Vec3(Vec3),
243    Vec4(Vec4), // used as color
244    Choice(usize),
245}
246
247#[derive(Debug, Clone)]
248pub struct PostFxParam {
249    pub name: String,
250    pub value: PostFxParamValue,
251    pub min_float: f32,
252    pub max_float: f32,
253    pub choices: Vec<String>,
254}
255
256impl PostFxParam {
257    pub fn float(name: &str, default: f32, min: f32, max: f32) -> Self {
258        Self { name: name.to_string(), value: PostFxParamValue::Float(default), min_float: min, max_float: max, choices: vec![] }
259    }
260    pub fn int(name: &str, default: i32, min: i32, max: i32) -> Self {
261        Self { name: name.to_string(), value: PostFxParamValue::Int(default), min_float: min as f32, max_float: max as f32, choices: vec![] }
262    }
263    pub fn toggle(name: &str, default: bool) -> Self {
264        Self { name: name.to_string(), value: PostFxParamValue::Bool(default), min_float: 0.0, max_float: 1.0, choices: vec![] }
265    }
266    pub fn color(name: &str, default: Vec4) -> Self {
267        Self { name: name.to_string(), value: PostFxParamValue::Vec4(default), min_float: 0.0, max_float: 1.0, choices: vec![] }
268    }
269    pub fn vec2(name: &str, default: Vec2) -> Self {
270        Self { name: name.to_string(), value: PostFxParamValue::Vec2(default), min_float: 0.0, max_float: 1.0, choices: vec![] }
271    }
272    pub fn choice(name: &str, default: usize, options: &[&str]) -> Self {
273        Self { name: name.to_string(), value: PostFxParamValue::Choice(default), min_float: 0.0, max_float: options.len() as f32, choices: options.iter().map(|s| s.to_string()).collect() }
274    }
275
276    pub fn as_float(&self) -> f32 {
277        match &self.value {
278            PostFxParamValue::Float(v) => *v,
279            PostFxParamValue::Int(v) => *v as f32,
280            _ => 0.0,
281        }
282    }
283
284    pub fn set_float(&mut self, v: f32) {
285        match &mut self.value {
286            PostFxParamValue::Float(x) => *x = v.clamp(self.min_float, self.max_float),
287            PostFxParamValue::Int(x) => *x = v.round() as i32,
288            _ => {}
289        }
290    }
291}
292
293// ---------------------------------------------------------------------------
294// PostFx Effect instance
295// ---------------------------------------------------------------------------
296
297#[derive(Debug, Clone)]
298pub struct PostFxEffect {
299    pub id: u32,
300    pub kind: PostFxKind,
301    pub enabled: bool,
302    pub params: Vec<PostFxParam>,
303    pub blend_weight: f32,
304    pub priority: i32,
305    pub name_override: Option<String>,
306}
307
308impl PostFxEffect {
309    pub fn new(id: u32, kind: PostFxKind) -> Self {
310        Self {
311            id,
312            params: kind.default_params(),
313            kind,
314            enabled: true,
315            blend_weight: 1.0,
316            priority: 0,
317            name_override: None,
318        }
319    }
320
321    pub fn display_name(&self) -> &str {
322        self.name_override.as_deref().unwrap_or_else(|| self.kind.label())
323    }
324
325    pub fn param(&self, name: &str) -> Option<&PostFxParam> {
326        self.params.iter().find(|p| p.name == name)
327    }
328
329    pub fn param_mut(&mut self, name: &str) -> Option<&mut PostFxParam> {
330        self.params.iter_mut().find(|p| p.name == name)
331    }
332
333    pub fn set_float(&mut self, name: &str, value: f32) {
334        if let Some(p) = self.param_mut(name) {
335            p.set_float(value);
336        }
337    }
338
339    pub fn get_float(&self, name: &str) -> f32 {
340        self.param(name).map(|p| p.as_float()).unwrap_or(0.0)
341    }
342
343    pub fn glsl_defines(&self) -> String {
344        let mut out = String::new();
345        out.push_str(&format!("#define POST_FX_{:?}\n", self.kind));
346        for p in &self.params {
347            match &p.value {
348                PostFxParamValue::Float(v) => out.push_str(&format!("#define PARAM_{} {:.6}\n", p.name.to_uppercase(), v)),
349                PostFxParamValue::Int(v) => out.push_str(&format!("#define PARAM_{} {}\n", p.name.to_uppercase(), v)),
350                PostFxParamValue::Bool(v) => out.push_str(&format!("#define PARAM_{} {}\n", p.name.to_uppercase(), if *v { 1 } else { 0 })),
351                _ => {}
352            }
353        }
354        out
355    }
356}
357
358// ---------------------------------------------------------------------------
359// Volume blending (like Unity's Volume system)
360// ---------------------------------------------------------------------------
361
362#[derive(Debug, Clone)]
363pub struct PostFxVolume {
364    pub name: String,
365    pub is_global: bool,
366    pub priority: f32,
367    pub blend_distance: f32,
368    pub weight: f32,
369    pub position: Vec3,
370    pub radius: f32,
371    pub effects: Vec<PostFxEffect>,
372    pub enabled: bool,
373}
374
375impl PostFxVolume {
376    pub fn new_global(name: impl Into<String>) -> Self {
377        Self {
378            name: name.into(),
379            is_global: true,
380            priority: 0.0,
381            blend_distance: 0.0,
382            weight: 1.0,
383            position: Vec3::ZERO,
384            radius: 0.0,
385            effects: Vec::new(),
386            enabled: true,
387        }
388    }
389
390    pub fn new_local(name: impl Into<String>, position: Vec3, radius: f32) -> Self {
391        Self {
392            name: name.into(),
393            is_global: false,
394            priority: 1.0,
395            blend_distance: 2.0,
396            weight: 1.0,
397            position,
398            radius,
399            effects: Vec::new(),
400            enabled: true,
401        }
402    }
403
404    pub fn influence_at(&self, point: Vec3) -> f32 {
405        if self.is_global { return self.weight; }
406        let dist = point.distance(self.position);
407        if dist >= self.radius + self.blend_distance { return 0.0; }
408        if dist <= self.radius { return self.weight; }
409        let t = 1.0 - (dist - self.radius) / self.blend_distance.max(0.001);
410        t * self.weight
411    }
412
413    pub fn add_effect(&mut self, kind: PostFxKind) -> u32 {
414        let id = self.effects.len() as u32 + 1;
415        self.effects.push(PostFxEffect::new(id, kind));
416        id
417    }
418}
419
420// ---------------------------------------------------------------------------
421// Post-processing stack
422// ---------------------------------------------------------------------------
423
424#[derive(Debug, Clone)]
425pub struct PostFxStack {
426    pub volumes: Vec<PostFxVolume>,
427    pub camera_position: Vec3,
428    pub enabled: bool,
429    pub render_scale: f32,
430    pub output_width: u32,
431    pub output_height: u32,
432}
433
434impl PostFxStack {
435    pub fn new() -> Self {
436        let mut stack = Self {
437            volumes: Vec::new(),
438            camera_position: Vec3::ZERO,
439            enabled: true,
440            render_scale: 1.0,
441            output_width: 1920,
442            output_height: 1080,
443        };
444        // Default global volume
445        let mut global = PostFxVolume::new_global("Global");
446        global.add_effect(PostFxKind::ToneMapping);
447        global.add_effect(PostFxKind::Bloom);
448        global.add_effect(PostFxKind::Vignette);
449        global.add_effect(PostFxKind::FilmGrain);
450        stack.volumes.push(global);
451        stack
452    }
453
454    pub fn active_volumes(&self) -> Vec<(&PostFxVolume, f32)> {
455        let mut result: Vec<(&PostFxVolume, f32)> = self.volumes.iter()
456            .filter(|v| v.enabled)
457            .map(|v| (v, v.influence_at(self.camera_position)))
458            .filter(|(_, influence)| *influence > 0.0)
459            .collect();
460        result.sort_by(|a, b| a.0.priority.partial_cmp(&b.0.priority).unwrap_or(std::cmp::Ordering::Equal));
461        result
462    }
463
464    pub fn compute_blended_param(&self, kind: PostFxKind, param_name: &str) -> f32 {
465        let mut blended = 0.0_f32;
466        let mut total_weight = 0.0_f32;
467        for (vol, influence) in self.active_volumes() {
468            if let Some(fx) = vol.effects.iter().find(|e| e.kind == kind && e.enabled) {
469                let v = fx.get_float(param_name);
470                blended += v * influence;
471                total_weight += influence;
472            }
473        }
474        if total_weight > 0.0 { blended / total_weight } else { 0.0 }
475    }
476
477    pub fn generate_shader_permutation_key(&self) -> u64 {
478        let active = self.active_volumes();
479        let mut key = 0u64;
480        for (vol, _) in &active {
481            for fx in &vol.effects {
482                if fx.enabled {
483                    key ^= fx.kind as u64 * 2654435761;
484                    key = key.rotate_left(7);
485                }
486            }
487        }
488        key
489    }
490}
491
492// ---------------------------------------------------------------------------
493// Post-FX editor panel
494// ---------------------------------------------------------------------------
495
496#[derive(Debug, Clone, Copy, PartialEq)]
497pub enum PostFxPreviewMode {
498    Final,
499    BeforePost,
500    SplitScreen,
501    Channel(u8), // R/G/B/A = 0/1/2/3
502    Depth,
503    Normals,
504    Albedo,
505    Roughness,
506    Ao,
507}
508
509#[derive(Debug, Clone)]
510pub struct PostFxEditorState {
511    pub stack: PostFxStack,
512    pub selected_volume: Option<usize>,
513    pub selected_effect: Option<u32>,
514    pub preview_mode: PostFxPreviewMode,
515    pub search_query: String,
516    pub show_all_categories: bool,
517    pub category_filter: Option<String>,
518    pub split_x: f32,
519    pub history: Vec<PostFxStack>,
520    pub history_pos: usize,
521}
522
523impl PostFxEditorState {
524    pub fn new() -> Self {
525        Self {
526            stack: PostFxStack::new(),
527            selected_volume: Some(0),
528            selected_effect: None,
529            preview_mode: PostFxPreviewMode::Final,
530            search_query: String::new(),
531            show_all_categories: true,
532            category_filter: None,
533            split_x: 0.5,
534            history: Vec::new(),
535            history_pos: 0,
536        }
537    }
538
539    pub fn snapshot(&mut self) {
540        self.history.truncate(self.history_pos);
541        self.history.push(self.stack.clone());
542        self.history_pos = self.history.len();
543    }
544
545    pub fn undo(&mut self) {
546        if self.history_pos > 1 {
547            self.history_pos -= 1;
548            self.stack = self.history[self.history_pos - 1].clone();
549        }
550    }
551
552    pub fn redo(&mut self) {
553        if self.history_pos < self.history.len() {
554            self.stack = self.history[self.history_pos].clone();
555            self.history_pos += 1;
556        }
557    }
558
559    pub fn selected_volume_mut(&mut self) -> Option<&mut PostFxVolume> {
560        self.selected_volume.and_then(|i| self.stack.volumes.get_mut(i))
561    }
562
563    pub fn add_effect_to_selected(&mut self, kind: PostFxKind) {
564        self.snapshot();
565        if let Some(vol) = self.selected_volume_mut() {
566            let id = vol.add_effect(kind);
567            self.selected_effect = Some(id);
568        }
569    }
570
571    pub fn remove_selected_effect(&mut self) {
572        self.snapshot();
573        if let (Some(vi), Some(eid)) = (self.selected_volume, self.selected_effect) {
574            if let Some(vol) = self.stack.volumes.get_mut(vi) {
575                vol.effects.retain(|e| e.id != eid);
576            }
577            self.selected_effect = None;
578        }
579    }
580
581    pub fn move_effect_up(&mut self) {
582        if let (Some(vi), Some(eid)) = (self.selected_volume, self.selected_effect) {
583            if let Some(vol) = self.stack.volumes.get_mut(vi) {
584                if let Some(i) = vol.effects.iter().position(|e| e.id == eid) {
585                    if i > 0 { vol.effects.swap(i, i - 1); }
586                }
587            }
588        }
589    }
590
591    pub fn move_effect_down(&mut self) {
592        if let (Some(vi), Some(eid)) = (self.selected_volume, self.selected_effect) {
593            if let Some(vol) = self.stack.volumes.get_mut(vi) {
594                if let Some(i) = vol.effects.iter().position(|e| e.id == eid) {
595                    if i + 1 < vol.effects.len() { vol.effects.swap(i, i + 1); }
596                }
597            }
598        }
599    }
600
601    pub fn set_effect_param(&mut self, param_name: &str, value: f32) {
602        self.snapshot();
603        if let (Some(vi), Some(eid)) = (self.selected_volume, self.selected_effect) {
604            if let Some(vol) = self.stack.volumes.get_mut(vi) {
605                if let Some(fx) = vol.effects.iter_mut().find(|e| e.id == eid) {
606                    fx.set_float(param_name, value);
607                }
608            }
609        }
610    }
611
612    pub fn search_effects(&self, query: &str) -> Vec<PostFxKind> {
613        use PostFxKind::*;
614        let all = [
615            Exposure, ToneMapping, ColorGrade, LiftGammaGain, HueSaturation, ColorBalance, Curves,
616            Bloom, LensFlare, LightShafts, DepthOfField, TiltShift, MotionBlur, TemporalAA,
617            Ssao, Hbao, ScreenSpaceReflections, Vignette, FilmGrain, ChromaticAberration,
618            LensDistortion, Scanlines, Pixelate, Posterize, Crosshatch, Halftone, Outline,
619            GaussianBlur, RadialBlur, DirectionalBlur, Sepia, Grayscale, Infrared, NightVision,
620            ThermalVision, Fxaa, Smaa, Taa, AutoExposure, HistogramEq, Underwater, RainDrops,
621            Fog, HeatHaze, Sharpen, UnsharpMask, EdgeEnhance, Crt, VhsGlitch, DigitalGlitch,
622        ];
623        let q = query.to_lowercase();
624        all.iter().copied().filter(|k| {
625            let cat_match = self.category_filter.as_deref().map(|c| k.category() == c).unwrap_or(true);
626            let text_match = q.is_empty() || k.label().to_lowercase().contains(&q) || k.category().to_lowercase().contains(&q);
627            cat_match && text_match
628        }).collect()
629    }
630
631    pub fn categories() -> Vec<&'static str> {
632        vec!["Color", "Glow", "Depth of Field", "Motion", "Ambient", "Film", "Stylistic",
633             "Blur", "Filter", "Anti-Aliasing", "HDR", "Weather", "Sharpen", "Retro"]
634    }
635
636    pub fn generate_full_pass_glsl(&self) -> String {
637        let mut src = String::from("// AUTO-GENERATED POST-PROCESS PASS\n");
638        src.push_str("#version 450\n");
639        src.push_str("layout(location=0) in vec2 v_uv;\n");
640        src.push_str("layout(location=0) out vec4 out_color;\n");
641        src.push_str("layout(binding=0) uniform sampler2D u_hdr;\n");
642        src.push_str("layout(binding=1) uniform sampler2D u_depth;\n");
643        src.push_str("layout(binding=2) uniform sampler2D u_normals;\n\n");
644        // Emit per-volume defines
645        for (vol, influence) in self.stack.active_volumes() {
646            if influence < 0.001 { continue; }
647            for fx in &vol.effects {
648                if fx.enabled {
649                    src.push_str(&fx.glsl_defines());
650                }
651            }
652        }
653        src.push_str("void main() {\n");
654        src.push_str("    vec4 color = texture(u_hdr, v_uv);\n");
655        src.push_str("    // ... pass chain evaluated in C++ shader pipeline\n");
656        src.push_str("    out_color = color;\n");
657        src.push_str("}\n");
658        src
659    }
660}
661
662// ---------------------------------------------------------------------------
663// Tests
664// ---------------------------------------------------------------------------
665#[cfg(test)]
666mod tests {
667    use super::*;
668
669    #[test]
670    fn test_postfx_defaults() {
671        let fx = PostFxEffect::new(1, PostFxKind::Bloom);
672        assert!(fx.get_float("threshold") > 0.0);
673        assert!(fx.enabled);
674    }
675
676    #[test]
677    fn test_volume_influence() {
678        let global = PostFxVolume::new_global("G");
679        assert!((global.influence_at(Vec3::ZERO) - 1.0).abs() < 1e-6);
680        let local = PostFxVolume::new_local("L", Vec3::ZERO, 5.0);
681        assert!((local.influence_at(Vec3::ZERO) - 1.0).abs() < 1e-6);
682        assert_eq!(local.influence_at(Vec3::new(10.0, 0.0, 0.0)), 0.0);
683    }
684
685    #[test]
686    fn test_stack_blending() {
687        let stack = PostFxStack::new();
688        let _v = stack.compute_blended_param(PostFxKind::Bloom, "threshold");
689    }
690
691    #[test]
692    fn test_editor_undo_redo() {
693        let mut ed = PostFxEditorState::new();
694        ed.add_effect_to_selected(PostFxKind::Fog);
695        let count_before = ed.stack.volumes[0].effects.len();
696        ed.undo();
697        let count_after = ed.stack.volumes[0].effects.len();
698        assert!(count_after <= count_before);
699    }
700
701    #[test]
702    fn test_search_effects() {
703        let ed = PostFxEditorState::new();
704        let results = ed.search_effects("blur");
705        assert!(!results.is_empty());
706    }
707}