proof_engine/render/postfx/
bloom_pipeline.rs1use super::bloom::{BloomParams, BloomPyramidLevel, compute_pyramid, normalise_pyramid_weights};
22
23pub struct EmissionPresets;
28
29impl EmissionPresets {
30 pub const UI_TEXT: f32 = 0.0;
33 pub const UI_HIGHLIGHT: f32 = 0.1;
35
36 pub const WORLD_GLYPH: f32 = 0.0;
39 pub const CHAOS_FIELD: f32 = 0.35;
41 pub const CHAOS_FIELD_INTENSE: f32 = 0.5;
43 pub const LIGHT_SOURCE: f32 = 0.8;
45
46 pub const DAMAGE_NUMBER: f32 = 1.5;
49 pub const CRIT_NUMBER: f32 = 2.5;
51 pub const HEAL_NUMBER: f32 = 1.2;
53 pub const MISS_TEXT: f32 = 0.3;
55
56 pub const SPELL_EFFECT: f32 = 2.0;
59 pub const SPELL_IMPACT: f32 = 3.0;
61 pub const BUFF_AURA: f32 = 1.0;
63
64 pub const BOSS_AURA: f32 = 1.0;
67 pub const BOSS_ATTACK: f32 = 2.0;
69 pub const BOSS_PHASE_CHANGE: f32 = 3.5;
71
72 pub const SHRINE: f32 = 1.5;
75 pub const PORTAL: f32 = 2.0;
77 pub const CORRUPTION: f32 = 0.8;
79 pub const LOOT_SPARKLE: f32 = 1.8;
81}
82
83pub struct ThemeBloom;
87
88impl ThemeBloom {
89 pub fn void_protocol() -> BloomParams {
91 BloomParams {
92 enabled: true,
93 threshold: 0.8,
94 intensity: 0.4,
95 radius: 3.0,
96 levels: 3,
97 knee: 0.1,
98 use_emission: true,
99 emission_weight: 1.0,
100 }
101 }
102
103 pub fn solar_forge() -> BloomParams {
105 BloomParams {
106 enabled: true,
107 threshold: 0.5,
108 intensity: 2.0,
109 radius: 6.0,
110 levels: 5,
111 knee: 0.15,
112 use_emission: true,
113 emission_weight: 2.0,
114 }
115 }
116
117 pub fn neon_grid() -> BloomParams {
119 BloomParams {
120 enabled: true,
121 threshold: 0.6,
122 intensity: 1.5,
123 radius: 4.0,
124 levels: 4,
125 knee: 0.1,
126 use_emission: true,
127 emission_weight: 1.8,
128 }
129 }
130
131 pub fn corruption(level: f32) -> BloomParams {
133 let corruption_boost = (level / 1000.0).clamp(0.0, 1.0);
134 BloomParams {
135 enabled: true,
136 threshold: 0.7 - corruption_boost * 0.3,
137 intensity: 0.6 + corruption_boost * 1.5,
138 radius: 4.0 + corruption_boost * 3.0,
139 levels: 4,
140 knee: 0.1 + corruption_boost * 0.1,
141 use_emission: true,
142 emission_weight: 1.2 + corruption_boost * 0.8,
143 }
144 }
145
146 pub fn boss_fight() -> BloomParams {
148 BloomParams {
149 enabled: true,
150 threshold: 0.6,
151 intensity: 1.8,
152 radius: 5.0,
153 levels: 4,
154 knee: 0.12,
155 use_emission: true,
156 emission_weight: 1.5,
157 }
158 }
159
160 pub fn death(progress: f32) -> BloomParams {
162 BloomParams {
163 enabled: true,
164 threshold: 0.5 + progress * 0.4,
165 intensity: 1.0 * (1.0 - progress),
166 radius: 4.0,
167 levels: 3,
168 knee: 0.1,
169 use_emission: true,
170 emission_weight: 1.0 * (1.0 - progress * 0.5),
171 }
172 }
173
174 pub fn shrine() -> BloomParams {
176 BloomParams {
177 enabled: true,
178 threshold: 0.5,
179 intensity: 1.2,
180 radius: 6.0,
181 levels: 5,
182 knee: 0.2,
183 use_emission: true,
184 emission_weight: 1.5,
185 }
186 }
187}
188
189#[derive(Debug, Clone)]
196pub struct BloomPipelineState {
197 pub params: BloomParams,
199 pub levels: Vec<BloomPyramidLevel>,
201 pub base_width: u32,
203 pub base_height: u32,
204 pub draw_call_count: u32,
206}
207
208impl BloomPipelineState {
209 pub fn compute(params: &BloomParams, width: u32, height: u32) -> Self {
211 let mut levels = compute_pyramid(width, height, params);
212 normalise_pyramid_weights(&mut levels);
213
214 let draw_call_count = if params.enabled {
217 1 + levels.len() as u32 * 4 + 1
218 } else {
219 0
220 };
221
222 Self {
223 params: params.clone(),
224 levels,
225 base_width: width,
226 base_height: height,
227 draw_call_count,
228 }
229 }
230
231 pub fn is_active(&self) -> bool { self.params.enabled && !self.levels.is_empty() }
233
234 pub fn level_count(&self) -> usize { self.levels.len() }
236}
237
238pub const BLOOM_THRESHOLD_FRAG: &str = r#"
245#version 330 core
246
247in vec2 f_uv;
248out vec4 frag_color;
249
250uniform sampler2D u_emission;
251uniform float u_threshold;
252uniform float u_knee;
253uniform float u_emission_weight;
254
255const vec3 LUMA = vec3(0.2126, 0.7152, 0.0722);
256
257void main() {
258 vec3 emiss = texture(u_emission, f_uv).rgb * u_emission_weight;
259 float lum = dot(emiss, LUMA);
260
261 // Soft threshold with knee
262 float lo = u_threshold - u_knee;
263 float hi = u_threshold + u_knee;
264 float weight;
265 if (lum <= lo) weight = 0.0;
266 else if (lum >= hi) weight = 1.0;
267 else {
268 float t = (lum - lo) / (2.0 * u_knee + 0.0001);
269 weight = t * t * (3.0 - 2.0 * t);
270 }
271
272 frag_color = vec4(emiss * weight, 1.0);
273}
274"#;
275
276pub const BLOOM_DOWNSAMPLE_FRAG: &str = r#"
279#version 330 core
280
281in vec2 f_uv;
282out vec4 frag_color;
283
284uniform sampler2D u_texture;
285uniform vec2 u_texel_size;
286
287void main() {
288 // 4-tap box downsample (tent filter)
289 vec3 a = texture(u_texture, f_uv + u_texel_size * vec2(-0.5, -0.5)).rgb;
290 vec3 b = texture(u_texture, f_uv + u_texel_size * vec2( 0.5, -0.5)).rgb;
291 vec3 c = texture(u_texture, f_uv + u_texel_size * vec2(-0.5, 0.5)).rgb;
292 vec3 d = texture(u_texture, f_uv + u_texel_size * vec2( 0.5, 0.5)).rgb;
293 frag_color = vec4((a + b + c + d) * 0.25, 1.0);
294}
295"#;
296
297pub const BLOOM_UPSAMPLE_FRAG: &str = r#"
300#version 330 core
301
302in vec2 f_uv;
303out vec4 frag_color;
304
305uniform sampler2D u_lower_mip; // smaller, more blurred
306uniform sampler2D u_current_mip; // current resolution
307uniform float u_weight; // blend weight for the lower mip
308
309void main() {
310 vec3 lower = texture(u_lower_mip, f_uv).rgb;
311 vec3 current = texture(u_current_mip, f_uv).rgb;
312 frag_color = vec4(current + lower * u_weight, 1.0);
313}
314"#;
315
316pub const BLOOM_BLUR_FRAG: &str = r#"
319#version 330 core
320
321in vec2 f_uv;
322out vec4 frag_color;
323
324uniform sampler2D u_texture;
325uniform vec2 u_texel_size;
326uniform vec2 u_direction;
327uniform float u_sigma;
328
329// 9-tap kernel (bilinear optimized to 5 taps)
330const int N_TAPS = 5;
331const float OFFSETS[5] = float[](0.0, 1.3846, 3.2308, 5.0769, 6.9231);
332const float WEIGHTS[5] = float[](0.2270, 0.3162, 0.0703, 0.0162, 0.0054);
333
334void main() {
335 vec3 result = texture(u_texture, f_uv).rgb * WEIGHTS[0];
336 float scale = u_sigma / 1.5;
337
338 for (int i = 1; i < N_TAPS; ++i) {
339 vec2 off = u_direction * u_texel_size * OFFSETS[i] * scale;
340 result += texture(u_texture, f_uv + off).rgb * WEIGHTS[i];
341 result += texture(u_texture, f_uv - off).rgb * WEIGHTS[i];
342 }
343 frag_color = vec4(result, 1.0);
344}
345"#;
346
347#[derive(Debug, Clone, Default)]
351pub struct BloomStats {
352 pub enabled: bool,
353 pub levels: u8,
354 pub threshold: f32,
355 pub intensity: f32,
356 pub draw_calls: u32,
357 pub brightest_pixel_lum: f32,
358}
359
360#[cfg(test)]
363mod tests {
364 use super::*;
365
366 #[test]
367 fn emission_presets_hierarchy() {
368 assert_eq!(EmissionPresets::UI_TEXT, 0.0);
369 assert!(EmissionPresets::CHAOS_FIELD < EmissionPresets::DAMAGE_NUMBER);
370 assert!(EmissionPresets::DAMAGE_NUMBER < EmissionPresets::CRIT_NUMBER);
371 assert!(EmissionPresets::BOSS_AURA < EmissionPresets::BOSS_PHASE_CHANGE);
372 }
373
374 #[test]
375 fn theme_bloom_void_protocol_is_subtle() {
376 let params = ThemeBloom::void_protocol();
377 assert!(params.intensity < 1.0);
378 assert!(params.threshold > 0.7);
379 }
380
381 #[test]
382 fn theme_bloom_solar_forge_is_dramatic() {
383 let params = ThemeBloom::solar_forge();
384 assert!(params.intensity > 1.5);
385 assert!(params.levels >= 4);
386 }
387
388 #[test]
389 fn corruption_bloom_scales() {
390 let low = ThemeBloom::corruption(0.0);
391 let high = ThemeBloom::corruption(1000.0);
392 assert!(high.intensity > low.intensity);
393 assert!(high.threshold < low.threshold);
394 }
395
396 #[test]
397 fn pipeline_state_computes_levels() {
398 let params = BloomParams::default();
399 let state = BloomPipelineState::compute(¶ms, 1280, 720);
400 assert_eq!(state.levels.len(), params.levels as usize);
401 assert!(state.draw_call_count > 0);
402 }
403
404 #[test]
405 fn pipeline_state_disabled() {
406 let params = BloomParams::disabled();
407 let state = BloomPipelineState::compute(¶ms, 1280, 720);
408 assert!(!state.is_active());
409 assert_eq!(state.draw_call_count, 0);
410 }
411
412 #[test]
413 fn death_bloom_fades() {
414 let start = ThemeBloom::death(0.0);
415 let end = ThemeBloom::death(1.0);
416 assert!(end.intensity < start.intensity);
417 }
418}