1
2use glam::{Vec2, Vec3, Vec4};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum PostFxKind {
13 Exposure,
15 ToneMapping,
16 ColorGrade,
17 LiftGammaGain,
18 HueSaturation,
19 ColorBalance,
20 Curves,
21 Bloom,
23 LensFlare,
24 LightShafts,
25 DepthOfField,
27 TiltShift,
28 MotionBlur,
30 TemporalAA,
31 Ssao,
33 Hbao,
34 ScreenSpaceReflections,
35 Vignette,
37 FilmGrain,
38 ChromaticAberration,
39 LensDistortion,
40 Scanlines,
41 Pixelate,
43 Posterize,
44 Crosshatch,
45 Halftone,
46 Outline,
47 GaussianBlur,
49 RadialBlur,
50 DirectionalBlur,
51 Sepia,
53 Grayscale,
54 Infrared,
55 NightVision,
56 ThermalVision,
57 Fxaa,
59 Smaa,
60 Taa,
61 AutoExposure,
63 HistogramEq,
64 Underwater,
66 RainDrops,
67 Fog,
68 HeatHaze,
69 Sharpen,
71 UnsharpMask,
72 EdgeEnhance,
73 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#[derive(Debug, Clone)]
237pub enum PostFxParamValue {
238 Float(f32),
239 Int(i32),
240 Bool(bool),
241 Vec2(Vec2),
242 Vec3(Vec3),
243 Vec4(Vec4), 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#[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#[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#[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 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#[derive(Debug, Clone, Copy, PartialEq)]
497pub enum PostFxPreviewMode {
498 Final,
499 BeforePost,
500 SplitScreen,
501 Channel(u8), 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 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#[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}