proof_engine/render/postfx/
fxaa.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum FxaaQuality {
23 Low,
25 Medium,
27 High,
29 Off,
31}
32
33impl FxaaQuality {
34 pub fn search_steps(&self) -> u32 {
36 match self {
37 Self::Low => 5,
38 Self::Medium => 8,
39 Self::High => 12,
40 Self::Off => 0,
41 }
42 }
43
44 pub fn define_value(&self) -> u32 {
46 match self {
47 Self::Low => 10,
48 Self::Medium => 20,
49 Self::High => 39,
50 Self::Off => 0,
51 }
52 }
53
54 pub fn is_enabled(&self) -> bool { *self != Self::Off }
55}
56
57impl Default for FxaaQuality {
58 fn default() -> Self { Self::Medium }
59}
60
61#[derive(Debug, Clone)]
65pub struct FxaaParams {
66 pub quality: FxaaQuality,
68 pub edge_threshold: f32,
71 pub edge_threshold_min: f32,
74 pub subpixel: f32,
77}
78
79impl Default for FxaaParams {
80 fn default() -> Self {
81 Self {
82 quality: FxaaQuality::Medium,
83 edge_threshold: 0.166,
84 edge_threshold_min: 0.0625,
85 subpixel: 0.75,
86 }
87 }
88}
89
90impl FxaaParams {
91 pub fn high() -> Self {
93 Self {
94 quality: FxaaQuality::High,
95 edge_threshold: 0.063,
96 edge_threshold_min: 0.0312,
97 subpixel: 1.0,
98 }
99 }
100
101 pub fn low() -> Self {
103 Self {
104 quality: FxaaQuality::Low,
105 edge_threshold: 0.250,
106 edge_threshold_min: 0.0833,
107 subpixel: 0.5,
108 }
109 }
110
111 pub fn off() -> Self {
113 Self { quality: FxaaQuality::Off, ..Default::default() }
114 }
115
116 pub fn is_enabled(&self) -> bool { self.quality.is_enabled() }
117
118 pub fn should_disable_for_scanlines(&self, scanlines_enabled: bool) -> bool {
120 scanlines_enabled
121 }
122}
123
124pub const FXAA_FRAG: &str = r#"
136#version 330 core
137
138in vec2 f_uv;
139out vec4 frag_color;
140
141uniform sampler2D u_scene;
142uniform vec2 u_texel_size;
143uniform float u_edge_threshold;
144uniform float u_edge_threshold_min;
145uniform float u_subpixel;
146uniform int u_search_steps;
147
148const vec3 LUMA = vec3(0.299, 0.587, 0.114);
149
150float luma(vec3 c) {
151 return dot(c, LUMA);
152}
153
154void main() {
155 // ── Sample center and neighbors ─────────────────────────────────────
156 vec3 rgbM = texture(u_scene, f_uv).rgb;
157 float lumM = luma(rgbM);
158
159 float lumNW = luma(texture(u_scene, f_uv + vec2(-1.0, -1.0) * u_texel_size).rgb);
160 float lumNE = luma(texture(u_scene, f_uv + vec2( 1.0, -1.0) * u_texel_size).rgb);
161 float lumSW = luma(texture(u_scene, f_uv + vec2(-1.0, 1.0) * u_texel_size).rgb);
162 float lumSE = luma(texture(u_scene, f_uv + vec2( 1.0, 1.0) * u_texel_size).rgb);
163 float lumN = luma(texture(u_scene, f_uv + vec2( 0.0, -1.0) * u_texel_size).rgb);
164 float lumS = luma(texture(u_scene, f_uv + vec2( 0.0, 1.0) * u_texel_size).rgb);
165 float lumW = luma(texture(u_scene, f_uv + vec2(-1.0, 0.0) * u_texel_size).rgb);
166 float lumE = luma(texture(u_scene, f_uv + vec2( 1.0, 0.0) * u_texel_size).rgb);
167
168 // ── Edge detection ──────────────────────────────────────────────────
169 float lumMin = min(lumM, min(min(lumN, lumS), min(lumW, lumE)));
170 float lumMax = max(lumM, max(max(lumN, lumS), max(lumW, lumE)));
171 float lumRange = lumMax - lumMin;
172
173 // Skip if contrast is too low (not an edge)
174 if (lumRange < max(u_edge_threshold_min, lumMax * u_edge_threshold)) {
175 frag_color = vec4(rgbM, 1.0);
176 return;
177 }
178
179 // ── Subpixel aliasing test ──────────────────────────────────────────
180 float lumL = (lumN + lumS + lumW + lumE) * 0.25;
181 float rangeL = abs(lumL - lumM);
182 float blendL = max(0.0, (rangeL / lumRange) - 0.25) * (1.0 / 0.75);
183 blendL = min(blendL * blendL, 1.0) * u_subpixel;
184
185 // ── Determine edge direction ────────────────────────────────────────
186 float edgeH = abs(lumNW + lumNE - 2.0 * lumN)
187 + abs(lumW + lumE - 2.0 * lumM) * 2.0
188 + abs(lumSW + lumSE - 2.0 * lumS);
189 float edgeV = abs(lumNW + lumSW - 2.0 * lumW)
190 + abs(lumN + lumS - 2.0 * lumM) * 2.0
191 + abs(lumNE + lumSE - 2.0 * lumE);
192
193 bool isHorizontal = edgeH >= edgeV;
194
195 // ── Choose edge endpoints ───────────────────────────────────────────
196 float stepLength = isHorizontal ? u_texel_size.y : u_texel_size.x;
197
198 float lum1 = isHorizontal ? lumN : lumW;
199 float lum2 = isHorizontal ? lumS : lumE;
200 float grad1 = lum1 - lumM;
201 float grad2 = lum2 - lumM;
202
203 bool steeper1 = abs(grad1) >= abs(grad2);
204 float gradScaled = 0.25 * max(abs(grad1), abs(grad2));
205
206 if (!steeper1) stepLength = -stepLength;
207
208 // ── Edge search along perpendicular direction ───────────────────────
209 vec2 posN = f_uv;
210 vec2 posP = f_uv;
211
212 vec2 dir = isHorizontal ? vec2(u_texel_size.x, 0.0) : vec2(0.0, u_texel_size.y);
213
214 float halfStep = isHorizontal ? u_texel_size.y * 0.5 : u_texel_size.x * 0.5;
215 if (isHorizontal) {
216 posN.y += stepLength * 0.5;
217 posP.y += stepLength * 0.5;
218 } else {
219 posN.x += stepLength * 0.5;
220 posP.x += stepLength * 0.5;
221 }
222
223 float lumEnd1 = lumM;
224 float lumEnd2 = lumM;
225 bool reached1 = false;
226 bool reached2 = false;
227
228 for (int i = 0; i < u_search_steps; ++i) {
229 if (!reached1) {
230 posN -= dir;
231 lumEnd1 = luma(texture(u_scene, posN).rgb) - lumM;
232 reached1 = abs(lumEnd1) >= gradScaled;
233 }
234 if (!reached2) {
235 posP += dir;
236 lumEnd2 = luma(texture(u_scene, posP).rgb) - lumM;
237 reached2 = abs(lumEnd2) >= gradScaled;
238 }
239 if (reached1 && reached2) break;
240 }
241
242 // ── Compute final blend ─────────────────────────────────────────────
243 float distN = isHorizontal ? (f_uv.x - posN.x) : (f_uv.y - posN.y);
244 float distP = isHorizontal ? (posP.x - f_uv.x) : (posP.y - f_uv.y);
245 float dist = min(distN, distP);
246 float spanLength = distN + distP;
247
248 bool goodSpan = (distN < distP) ? (lumEnd1 < 0.0) : (lumEnd2 < 0.0);
249 float pixelOffset = goodSpan ? (0.5 - dist / spanLength) : 0.0;
250
251 float finalBlend = max(pixelOffset, blendL);
252
253 // ── Apply ───────────────────────────────────────────────────────────
254 vec2 finalUv = f_uv;
255 if (isHorizontal) {
256 finalUv.y += finalBlend * stepLength;
257 } else {
258 finalUv.x += finalBlend * stepLength;
259 }
260
261 frag_color = vec4(texture(u_scene, finalUv).rgb, 1.0);
262}
263"#;
264
265#[derive(Debug, Clone, Default)]
269pub struct FxaaStats {
270 pub enabled: bool,
271 pub quality: &'static str,
272 pub search_steps: u32,
273}
274
275impl FxaaStats {
276 pub fn from_params(params: &FxaaParams) -> Self {
277 Self {
278 enabled: params.is_enabled(),
279 quality: match params.quality {
280 FxaaQuality::Low => "Low",
281 FxaaQuality::Medium => "Medium",
282 FxaaQuality::High => "High",
283 FxaaQuality::Off => "Off",
284 },
285 search_steps: params.quality.search_steps(),
286 }
287 }
288}
289
290#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn default_is_medium() {
298 let params = FxaaParams::default();
299 assert_eq!(params.quality, FxaaQuality::Medium);
300 assert!(params.is_enabled());
301 }
302
303 #[test]
304 fn off_is_disabled() {
305 let params = FxaaParams::off();
306 assert!(!params.is_enabled());
307 assert_eq!(params.quality.search_steps(), 0);
308 }
309
310 #[test]
311 fn quality_search_steps() {
312 assert!(FxaaQuality::Low.search_steps() < FxaaQuality::Medium.search_steps());
313 assert!(FxaaQuality::Medium.search_steps() < FxaaQuality::High.search_steps());
314 }
315
316 #[test]
317 fn scanlines_conflict() {
318 let params = FxaaParams::default();
319 assert!(params.should_disable_for_scanlines(true));
320 assert!(!params.should_disable_for_scanlines(false));
321 }
322
323 #[test]
324 fn edge_thresholds_in_range() {
325 let high = FxaaParams::high();
326 let low = FxaaParams::low();
327 assert!(high.edge_threshold < low.edge_threshold);
328 assert!(high.edge_threshold_min < low.edge_threshold_min);
329 }
330
331 #[test]
332 fn stats_from_params() {
333 let stats = FxaaStats::from_params(&FxaaParams::default());
334 assert!(stats.enabled);
335 assert_eq!(stats.quality, "Medium");
336 assert_eq!(stats.search_steps, 8);
337 }
338}