1use std::fmt;
12
13use super::{Viewport, clampf, lerpf, saturate};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum AntiAliasingMode {
22 None,
24 Fxaa,
26 Taa,
28 Msaa,
30 FxaaPlusTaa,
32}
33
34impl AntiAliasingMode {
35 pub fn name(&self) -> &'static str {
36 match self {
37 Self::None => "None",
38 Self::Fxaa => "FXAA",
39 Self::Taa => "TAA",
40 Self::Msaa => "MSAA",
41 Self::FxaaPlusTaa => "FXAA+TAA",
42 }
43 }
44
45 pub fn next(&self) -> Self {
47 match self {
48 Self::None => Self::Fxaa,
49 Self::Fxaa => Self::Taa,
50 Self::Taa => Self::Msaa,
51 Self::Msaa => Self::FxaaPlusTaa,
52 Self::FxaaPlusTaa => Self::None,
53 }
54 }
55}
56
57impl Default for AntiAliasingMode {
58 fn default() -> Self {
59 Self::Fxaa
60 }
61}
62
63impl fmt::Display for AntiAliasingMode {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 write!(f, "{}", self.name())
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum FxaaQuality {
76 Low,
78 Medium,
80 High,
82 Ultra,
84}
85
86impl FxaaQuality {
87 pub fn search_steps(&self) -> u32 {
89 match self {
90 Self::Low => 3,
91 Self::Medium => 5,
92 Self::High => 8,
93 Self::Ultra => 12,
94 }
95 }
96
97 pub fn edge_threshold(&self) -> f32 {
99 match self {
100 Self::Low => 0.250,
101 Self::Medium => 0.166,
102 Self::High => 0.125,
103 Self::Ultra => 0.063,
104 }
105 }
106
107 pub fn edge_threshold_min(&self) -> f32 {
109 match self {
110 Self::Low => 0.0833,
111 Self::Medium => 0.0625,
112 Self::High => 0.0312,
113 Self::Ultra => 0.0156,
114 }
115 }
116
117 pub fn subpixel_quality(&self) -> f32 {
119 match self {
120 Self::Low => 0.50,
121 Self::Medium => 0.75,
122 Self::High => 0.875,
123 Self::Ultra => 1.0,
124 }
125 }
126
127 pub fn search_step_sizes(&self) -> Vec<f32> {
129 match self {
130 Self::Low => vec![1.0, 1.5, 2.0],
131 Self::Medium => vec![1.0, 1.0, 1.0, 1.5, 2.0],
132 Self::High => vec![1.0, 1.0, 1.0, 1.0, 1.0, 1.5, 2.0, 4.0],
133 Self::Ultra => vec![1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.5, 2.0, 2.0, 4.0, 8.0],
134 }
135 }
136}
137
138impl Default for FxaaQuality {
139 fn default() -> Self {
140 Self::High
141 }
142}
143
144impl fmt::Display for FxaaQuality {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 let name = match self {
147 Self::Low => "Low",
148 Self::Medium => "Medium",
149 Self::High => "High",
150 Self::Ultra => "Ultra",
151 };
152 write!(f, "{}", name)
153 }
154}
155
156#[derive(Debug)]
158pub struct FxaaPass {
159 pub enabled: bool,
161 pub quality: FxaaQuality,
163 pub shader_handle: u64,
165 pub time_us: u64,
167 pub custom_edge_threshold: f32,
169 pub custom_subpixel_quality: f32,
171 pub show_edges: bool,
173}
174
175impl FxaaPass {
176 pub fn new() -> Self {
177 Self {
178 enabled: true,
179 quality: FxaaQuality::High,
180 shader_handle: 0,
181 time_us: 0,
182 custom_edge_threshold: 0.0,
183 custom_subpixel_quality: -1.0,
184 show_edges: false,
185 }
186 }
187
188 pub fn with_quality(mut self, quality: FxaaQuality) -> Self {
189 self.quality = quality;
190 self
191 }
192
193 pub fn edge_threshold(&self) -> f32 {
195 if self.custom_edge_threshold > 0.0 {
196 self.custom_edge_threshold
197 } else {
198 self.quality.edge_threshold()
199 }
200 }
201
202 pub fn subpixel_quality(&self) -> f32 {
204 if self.custom_subpixel_quality >= 0.0 {
205 self.custom_subpixel_quality
206 } else {
207 self.quality.subpixel_quality()
208 }
209 }
210
211 pub fn luminance(r: f32, g: f32, b: f32) -> f32 {
213 0.299 * r + 0.587 * g + 0.114 * b
214 }
215
216 pub fn process_pixel<F>(
219 &self,
220 center_x: u32,
221 center_y: u32,
222 width: u32,
223 height: u32,
224 sample: F,
225 ) -> [f32; 3]
226 where
227 F: Fn(i32, i32) -> [f32; 3],
228 {
229 let c = sample(center_x as i32, center_y as i32);
230 let lum_c = Self::luminance(c[0], c[1], c[2]);
231
232 let n = sample(center_x as i32, center_y as i32 - 1);
234 let s = sample(center_x as i32, center_y as i32 + 1);
235 let e = sample(center_x as i32 + 1, center_y as i32);
236 let w = sample(center_x as i32 - 1, center_y as i32);
237
238 let lum_n = Self::luminance(n[0], n[1], n[2]);
239 let lum_s = Self::luminance(s[0], s[1], s[2]);
240 let lum_e = Self::luminance(e[0], e[1], e[2]);
241 let lum_w = Self::luminance(w[0], w[1], w[2]);
242
243 let lum_min = lum_c.min(lum_n).min(lum_s).min(lum_e).min(lum_w);
244 let lum_max = lum_c.max(lum_n).max(lum_s).max(lum_e).max(lum_w);
245 let lum_range = lum_max - lum_min;
246
247 let threshold = self.edge_threshold();
249 let threshold_min = self.quality.edge_threshold_min();
250 if lum_range < threshold.max(threshold_min) {
251 return c; }
253
254 let ne = sample(center_x as i32 + 1, center_y as i32 - 1);
256 let nw = sample(center_x as i32 - 1, center_y as i32 - 1);
257 let se = sample(center_x as i32 + 1, center_y as i32 + 1);
258 let sw = sample(center_x as i32 - 1, center_y as i32 + 1);
259
260 let lum_ne = Self::luminance(ne[0], ne[1], ne[2]);
261 let lum_nw = Self::luminance(nw[0], nw[1], nw[2]);
262 let lum_se = Self::luminance(se[0], se[1], se[2]);
263 let lum_sw = Self::luminance(sw[0], sw[1], sw[2]);
264
265 let lum_avg = (lum_n + lum_s + lum_e + lum_w) * 0.25;
267 let subpixel_offset = saturate(
268 ((lum_avg - lum_c).abs() / lum_range.max(1e-6)) * self.subpixel_quality(),
269 );
270
271 let edge_h = (lum_nw + lum_ne - 2.0 * lum_n).abs()
273 + 2.0 * (lum_w + lum_e - 2.0 * lum_c).abs()
274 + (lum_sw + lum_se - 2.0 * lum_s).abs();
275 let edge_v = (lum_nw + lum_sw - 2.0 * lum_w).abs()
276 + 2.0 * (lum_n + lum_s - 2.0 * lum_c).abs()
277 + (lum_ne + lum_se - 2.0 * lum_e).abs();
278 let is_horizontal = edge_h >= edge_v;
279
280 let step_length = if is_horizontal {
282 1.0 / height as f32
283 } else {
284 1.0 / width as f32
285 };
286
287 let (lum_positive, lum_negative) = if is_horizontal {
288 (lum_s, lum_n)
289 } else {
290 (lum_e, lum_w)
291 };
292
293 let gradient_positive = (lum_positive - lum_c).abs();
294 let gradient_negative = (lum_negative - lum_c).abs();
295
296 let _step = if gradient_positive >= gradient_negative {
297 step_length
298 } else {
299 -step_length
300 };
301
302 let blend_factor = subpixel_offset * subpixel_offset;
304
305 let neighbor = if is_horizontal {
307 if gradient_positive >= gradient_negative { s } else { n }
308 } else {
309 if gradient_positive >= gradient_negative { e } else { w }
310 };
311
312 [
313 lerpf(c[0], neighbor[0], blend_factor),
314 lerpf(c[1], neighbor[1], blend_factor),
315 lerpf(c[2], neighbor[2], blend_factor),
316 ]
317 }
318
319 pub fn execute(&mut self, _viewport: &Viewport) {
321 let start = std::time::Instant::now();
322
323 if !self.enabled {
324 self.time_us = 0;
325 return;
326 }
327
328 self.time_us = start.elapsed().as_micros() as u64;
336 }
337
338 pub fn fragment_shader(&self) -> String {
340 let quality = &self.quality;
341 let steps = quality.search_steps();
342 let step_sizes = quality.search_step_sizes();
343
344 let mut shader = String::from(r#"#version 330 core
345in vec2 v_texcoord;
346out vec4 frag_color;
347
348uniform sampler2D u_scene;
349uniform vec2 u_texel_size;
350uniform float u_edge_threshold;
351uniform float u_edge_threshold_min;
352uniform float u_subpixel_quality;
353
354float luma(vec3 c) {
355 return dot(c, vec3(0.299, 0.587, 0.114));
356}
357
358void main() {
359 vec3 rgbM = texture(u_scene, v_texcoord).rgb;
360 float lumaM = luma(rgbM);
361
362 float lumaN = luma(texture(u_scene, v_texcoord + vec2(0.0, -u_texel_size.y)).rgb);
363 float lumaS = luma(texture(u_scene, v_texcoord + vec2(0.0, u_texel_size.y)).rgb);
364 float lumaE = luma(texture(u_scene, v_texcoord + vec2( u_texel_size.x, 0.0)).rgb);
365 float lumaW = luma(texture(u_scene, v_texcoord + vec2(-u_texel_size.x, 0.0)).rgb);
366
367 float lumaMin = min(lumaM, min(min(lumaN, lumaS), min(lumaE, lumaW)));
368 float lumaMax = max(lumaM, max(max(lumaN, lumaS), max(lumaE, lumaW)));
369 float lumaRange = lumaMax - lumaMin;
370
371 if (lumaRange < max(u_edge_threshold, u_edge_threshold_min)) {
372 frag_color = vec4(rgbM, 1.0);
373 return;
374 }
375
376 float lumaNE = luma(texture(u_scene, v_texcoord + vec2( u_texel_size.x, -u_texel_size.y)).rgb);
377 float lumaNW = luma(texture(u_scene, v_texcoord + vec2(-u_texel_size.x, -u_texel_size.y)).rgb);
378 float lumaSE = luma(texture(u_scene, v_texcoord + vec2( u_texel_size.x, u_texel_size.y)).rgb);
379 float lumaSW = luma(texture(u_scene, v_texcoord + vec2(-u_texel_size.x, u_texel_size.y)).rgb);
380
381 float edgeH = abs(lumaNW + lumaNE - 2.0*lumaN)
382 + 2.0*abs(lumaW + lumaE - 2.0*lumaM)
383 + abs(lumaSW + lumaSE - 2.0*lumaS);
384 float edgeV = abs(lumaNW + lumaSW - 2.0*lumaW)
385 + 2.0*abs(lumaN + lumaS - 2.0*lumaM)
386 + abs(lumaNE + lumaSE - 2.0*lumaE);
387 bool isHorizontal = edgeH >= edgeV;
388
389 float stepLength = isHorizontal ? u_texel_size.y : u_texel_size.x;
390 float lumaP = isHorizontal ? lumaS : lumaE;
391 float lumaN2 = isHorizontal ? lumaN : lumaW;
392 float gradP = abs(lumaP - lumaM);
393 float gradN = abs(lumaN2 - lumaM);
394 float step = (gradP >= gradN) ? stepLength : -stepLength;
395
396 vec2 edgeDir = isHorizontal ? vec2(u_texel_size.x, 0.0) : vec2(0.0, u_texel_size.y);
397 vec2 pos = v_texcoord;
398 if (isHorizontal) pos.y += step * 0.5;
399 else pos.x += step * 0.5;
400
401 float lumaEnd = (gradP >= gradN) ? lumaP : lumaN2;
402 float lumaLocalAvg = 0.5 * (lumaEnd + lumaM);
403 bool sign = (lumaLocalAvg - lumaM) >= 0.0;
404
405 // Edge search
406"#);
407
408 shader.push_str(&format!(
410 " vec2 posP = pos + edgeDir;\n vec2 posN = pos - edgeDir;\n"
411 ));
412 shader.push_str(
413 " float lumaEndP = luma(texture(u_scene, posP).rgb) - lumaLocalAvg;\n"
414 );
415 shader.push_str(
416 " float lumaEndN = luma(texture(u_scene, posN).rgb) - lumaLocalAvg;\n"
417 );
418 shader.push_str(" bool doneP = abs(lumaEndP) >= lumaRange * 0.25;\n");
419 shader.push_str(" bool doneN = abs(lumaEndN) >= lumaRange * 0.25;\n\n");
420
421 for i in 1..steps {
422 let step_size = if (i as usize) < step_sizes.len() {
423 step_sizes[i as usize]
424 } else {
425 1.0
426 };
427 shader.push_str(&format!(
428 " if (!doneP) posP += edgeDir * {:.1};\n",
429 step_size
430 ));
431 shader.push_str(&format!(
432 " if (!doneN) posN -= edgeDir * {:.1};\n",
433 step_size
434 ));
435 shader.push_str(
436 " if (!doneP) lumaEndP = luma(texture(u_scene, posP).rgb) - lumaLocalAvg;\n"
437 );
438 shader.push_str(
439 " if (!doneN) lumaEndN = luma(texture(u_scene, posN).rgb) - lumaLocalAvg;\n"
440 );
441 shader.push_str(" if (!doneP) doneP = abs(lumaEndP) >= lumaRange * 0.25;\n");
442 shader.push_str(" if (!doneN) doneN = abs(lumaEndN) >= lumaRange * 0.25;\n\n");
443 }
444
445 shader.push_str(r#"
446 float distP = isHorizontal ? (posP.x - v_texcoord.x) : (posP.y - v_texcoord.y);
447 float distN = isHorizontal ? (v_texcoord.x - posN.x) : (v_texcoord.y - posN.y);
448 float dist = min(distP, distN);
449 float spanLength = distP + distN;
450 float pixelOffset = -dist / spanLength + 0.5;
451
452 float lumaAvg = (1.0/12.0) * (2.0*(lumaN+lumaS+lumaE+lumaW) + lumaNE+lumaNW+lumaSE+lumaSW);
453 float subPixelDelta = clamp(abs(lumaAvg - lumaM) / lumaRange, 0.0, 1.0);
454 float subPixelOffset = (-2.0*subPixelDelta + 3.0)*subPixelDelta*subPixelDelta * u_subpixel_quality;
455 float finalOffset = max(pixelOffset, subPixelOffset);
456
457 vec2 finalUv = v_texcoord;
458 if (isHorizontal) finalUv.y += finalOffset * step;
459 else finalUv.x += finalOffset * step;
460
461 frag_color = vec4(texture(u_scene, finalUv).rgb, 1.0);
462}
463"#);
464
465 shader
466 }
467}
468
469impl Default for FxaaPass {
470 fn default() -> Self {
471 Self::new()
472 }
473}
474
475#[derive(Debug, Clone, Copy, PartialEq, Eq)]
481pub enum JitterSequence {
482 Halton23,
484 RotatedGrid8,
486 Halton16,
488 BlueNoise,
490}
491
492impl JitterSequence {
493 pub fn sample(&self, frame_index: u32) -> [f32; 2] {
496 match self {
497 Self::Halton23 => {
498 let x = halton(frame_index + 1, 2) - 0.5;
499 let y = halton(frame_index + 1, 3) - 0.5;
500 [x, y]
501 }
502 Self::RotatedGrid8 => {
503 let samples: [[f32; 2]; 8] = [
504 [-0.375, -0.375],
505 [ 0.125, -0.375],
506 [-0.125, -0.125],
507 [ 0.375, -0.125],
508 [-0.375, 0.125],
509 [ 0.125, 0.125],
510 [-0.125, 0.375],
511 [ 0.375, 0.375],
512 ];
513 let idx = (frame_index as usize) % 8;
514 samples[idx]
515 }
516 Self::Halton16 => {
517 let x = halton((frame_index % 16) + 1, 2) - 0.5;
518 let y = halton((frame_index % 16) + 1, 3) - 0.5;
519 [x, y]
520 }
521 Self::BlueNoise => {
522 let x = halton(frame_index * 7 + 1, 2) - 0.5;
524 let y = halton(frame_index * 11 + 1, 3) - 0.5;
525 [x, y]
526 }
527 }
528 }
529
530 pub fn length(&self) -> u32 {
532 match self {
533 Self::Halton23 => 256,
534 Self::RotatedGrid8 => 8,
535 Self::Halton16 => 16,
536 Self::BlueNoise => 256,
537 }
538 }
539}
540
541fn halton(mut index: u32, base: u32) -> f32 {
543 let mut result = 0.0f32;
544 let mut f = 1.0f32 / base as f32;
545 while index > 0 {
546 result += f * (index % base) as f32;
547 index /= base;
548 f /= base as f32;
549 }
550 result
551}
552
553#[derive(Debug, Clone)]
555pub struct TaaConfig {
556 pub jitter_sequence: JitterSequence,
558 pub history_blend: f32,
561 pub velocity_reprojection: bool,
563 pub neighborhood_clamping: bool,
565 pub clamp_gamma: f32,
567 pub variance_clipping: bool,
569 pub variance_clip_gamma: f32,
571 pub motion_rejection: bool,
573 pub motion_rejection_strength: f32,
575 pub sharpen_amount: f32,
577 pub catmull_rom_history: bool,
579 pub luminance_weighting: bool,
581 pub flicker_reduction: bool,
583 pub flicker_strength: f32,
585}
586
587impl TaaConfig {
588 pub fn new() -> Self {
589 Self {
590 jitter_sequence: JitterSequence::Halton23,
591 history_blend: 0.9,
592 velocity_reprojection: true,
593 neighborhood_clamping: true,
594 clamp_gamma: 1.0,
595 variance_clipping: false,
596 variance_clip_gamma: 1.0,
597 motion_rejection: true,
598 motion_rejection_strength: 0.5,
599 sharpen_amount: 0.0,
600 catmull_rom_history: true,
601 luminance_weighting: true,
602 flicker_reduction: false,
603 flicker_strength: 0.5,
604 }
605 }
606
607 pub fn high_quality() -> Self {
609 Self {
610 jitter_sequence: JitterSequence::Halton23,
611 history_blend: 0.95,
612 velocity_reprojection: true,
613 neighborhood_clamping: true,
614 clamp_gamma: 1.0,
615 variance_clipping: true,
616 variance_clip_gamma: 1.25,
617 motion_rejection: true,
618 motion_rejection_strength: 0.7,
619 sharpen_amount: 0.2,
620 catmull_rom_history: true,
621 luminance_weighting: true,
622 flicker_reduction: true,
623 flicker_strength: 0.5,
624 }
625 }
626
627 pub fn fast() -> Self {
629 Self {
630 jitter_sequence: JitterSequence::RotatedGrid8,
631 history_blend: 0.85,
632 velocity_reprojection: true,
633 neighborhood_clamping: true,
634 clamp_gamma: 1.5,
635 variance_clipping: false,
636 variance_clip_gamma: 1.0,
637 motion_rejection: false,
638 motion_rejection_strength: 0.0,
639 sharpen_amount: 0.0,
640 catmull_rom_history: false,
641 luminance_weighting: false,
642 flicker_reduction: false,
643 flicker_strength: 0.0,
644 }
645 }
646}
647
648impl Default for TaaConfig {
649 fn default() -> Self {
650 Self::new()
651 }
652}
653
654#[derive(Debug)]
656pub struct TaaPass {
657 pub enabled: bool,
659 pub config: TaaConfig,
661 pub shader_handle: u64,
663 pub history_handle: u64,
665 pub prev_history_handle: u64,
667 pub velocity_handle: u64,
669 pub current_jitter: [f32; 2],
671 pub frame_index: u32,
673 pub prev_view_proj: super::Mat4,
675 pub current_view_proj: super::Mat4,
677 pub history_width: u32,
679 pub history_height: u32,
680 pub history_valid: bool,
682 pub time_us: u64,
684 pub ping_pong: bool,
686}
687
688impl TaaPass {
689 pub fn new() -> Self {
690 Self {
691 enabled: true,
692 config: TaaConfig::new(),
693 shader_handle: 0,
694 history_handle: 0,
695 prev_history_handle: 0,
696 velocity_handle: 0,
697 current_jitter: [0.0, 0.0],
698 frame_index: 0,
699 prev_view_proj: super::Mat4::IDENTITY,
700 current_view_proj: super::Mat4::IDENTITY,
701 history_width: 0,
702 history_height: 0,
703 history_valid: false,
704 time_us: 0,
705 ping_pong: false,
706 }
707 }
708
709 pub fn with_config(mut self, config: TaaConfig) -> Self {
710 self.config = config;
711 self
712 }
713
714 pub fn begin_frame(&mut self, view_proj: &super::Mat4) {
716 self.prev_view_proj = self.current_view_proj;
717 self.current_view_proj = *view_proj;
718
719 self.current_jitter = self.config.jitter_sequence.sample(self.frame_index);
720 self.frame_index = (self.frame_index + 1) % self.config.jitter_sequence.length();
721 self.ping_pong = !self.ping_pong;
722 }
723
724 pub fn jitter_ndc(&self, viewport: &Viewport) -> [f32; 2] {
726 [
727 self.current_jitter[0] * 2.0 / viewport.width as f32,
728 self.current_jitter[1] * 2.0 / viewport.height as f32,
729 ]
730 }
731
732 pub fn jittered_projection(&self, proj: &super::Mat4, viewport: &Viewport) -> super::Mat4 {
734 let jitter = self.jitter_ndc(viewport);
735 let mut jittered = *proj;
736 jittered.cols[2][0] += jitter[0];
737 jittered.cols[2][1] += jitter[1];
738 jittered
739 }
740
741 pub fn resize(&mut self, width: u32, height: u32) {
743 if self.history_width != width || self.history_height != height {
744 self.history_width = width;
745 self.history_height = height;
746 self.history_valid = false;
747 }
749 }
750
751 pub fn invalidate_history(&mut self) {
753 self.history_valid = false;
754 }
755
756 pub fn neighborhood_clamp(
759 current_color: [f32; 3],
760 history_color: [f32; 3],
761 neighborhood_min: [f32; 3],
762 neighborhood_max: [f32; 3],
763 gamma: f32,
764 ) -> [f32; 3] {
765 let center = [
767 (neighborhood_min[0] + neighborhood_max[0]) * 0.5,
768 (neighborhood_min[1] + neighborhood_max[1]) * 0.5,
769 (neighborhood_min[2] + neighborhood_max[2]) * 0.5,
770 ];
771 let extent = [
772 (neighborhood_max[0] - neighborhood_min[0]) * 0.5 * gamma,
773 (neighborhood_max[1] - neighborhood_min[1]) * 0.5 * gamma,
774 (neighborhood_max[2] - neighborhood_min[2]) * 0.5 * gamma,
775 ];
776 let clamped_min = [
777 center[0] - extent[0],
778 center[1] - extent[1],
779 center[2] - extent[2],
780 ];
781 let clamped_max = [
782 center[0] + extent[0],
783 center[1] + extent[1],
784 center[2] + extent[2],
785 ];
786
787 let _ = current_color;
788
789 [
790 clampf(history_color[0], clamped_min[0], clamped_max[0]),
791 clampf(history_color[1], clamped_min[1], clamped_max[1]),
792 clampf(history_color[2], clamped_min[2], clamped_max[2]),
793 ]
794 }
795
796 pub fn variance_clip(
798 history_color: [f32; 3],
799 neighborhood_mean: [f32; 3],
800 neighborhood_variance: [f32; 3],
801 gamma: f32,
802 ) -> [f32; 3] {
803 let sigma = [
804 neighborhood_variance[0].sqrt() * gamma,
805 neighborhood_variance[1].sqrt() * gamma,
806 neighborhood_variance[2].sqrt() * gamma,
807 ];
808 [
809 clampf(
810 history_color[0],
811 neighborhood_mean[0] - sigma[0],
812 neighborhood_mean[0] + sigma[0],
813 ),
814 clampf(
815 history_color[1],
816 neighborhood_mean[1] - sigma[1],
817 neighborhood_mean[1] + sigma[1],
818 ),
819 clampf(
820 history_color[2],
821 neighborhood_mean[2] - sigma[2],
822 neighborhood_mean[2] + sigma[2],
823 ),
824 ]
825 }
826
827 pub fn compute_blend_factor(
829 &self,
830 velocity_length: f32,
831 ) -> f32 {
832 let mut blend = self.config.history_blend;
833
834 if self.config.motion_rejection && velocity_length > 0.001 {
836 let motion_factor = saturate(velocity_length * self.config.motion_rejection_strength * 100.0);
837 blend *= 1.0 - motion_factor;
838 }
839
840 if !self.history_valid {
842 return 0.0;
843 }
844
845 clampf(blend, 0.0, 0.98)
846 }
847
848 pub fn execute(&mut self, _viewport: &Viewport) {
850 let start = std::time::Instant::now();
851
852 if !self.enabled {
853 self.time_us = 0;
854 return;
855 }
856
857 self.history_valid = true;
865 self.time_us = start.elapsed().as_micros() as u64;
866 }
867
868 pub fn fragment_shader(&self) -> String {
870 let mut s = String::from(r#"#version 330 core
871in vec2 v_texcoord;
872out vec4 frag_color;
873
874uniform sampler2D u_current;
875uniform sampler2D u_history;
876uniform sampler2D u_velocity;
877uniform sampler2D u_depth;
878uniform vec2 u_texel_size;
879uniform float u_blend_factor;
880uniform mat4 u_prev_vp;
881uniform mat4 u_inv_vp;
882uniform vec2 u_jitter;
883uniform bool u_use_variance_clip;
884
885vec3 rgb_to_ycocg(vec3 rgb) {
886 return vec3(
887 0.25*rgb.r + 0.5*rgb.g + 0.25*rgb.b,
888 0.5*rgb.r - 0.5*rgb.b,
889 -0.25*rgb.r + 0.5*rgb.g - 0.25*rgb.b
890 );
891}
892
893vec3 ycocg_to_rgb(vec3 ycocg) {
894 return vec3(
895 ycocg.x + ycocg.y - ycocg.z,
896 ycocg.x + ycocg.z,
897 ycocg.x - ycocg.y - ycocg.z
898 );
899}
900
901void main() {
902 // Remove jitter from current frame UV
903 vec2 uv = v_texcoord - u_jitter * 0.5;
904
905 vec3 current = texture(u_current, uv).rgb;
906
907 // Reproject using velocity
908 vec2 velocity = texture(u_velocity, v_texcoord).rg;
909 vec2 history_uv = v_texcoord - velocity;
910
911 // Check if history UV is valid
912 if (history_uv.x < 0.0 || history_uv.x > 1.0 || history_uv.y < 0.0 || history_uv.y > 1.0) {
913 frag_color = vec4(current, 1.0);
914 return;
915 }
916
917 vec3 history = texture(u_history, history_uv).rgb;
918
919 // Neighborhood clamping in YCoCg space
920 vec3 s0 = rgb_to_ycocg(current);
921 vec3 s1 = rgb_to_ycocg(texture(u_current, uv + vec2(-u_texel_size.x, 0)).rgb);
922 vec3 s2 = rgb_to_ycocg(texture(u_current, uv + vec2( u_texel_size.x, 0)).rgb);
923 vec3 s3 = rgb_to_ycocg(texture(u_current, uv + vec2(0, -u_texel_size.y)).rgb);
924 vec3 s4 = rgb_to_ycocg(texture(u_current, uv + vec2(0, u_texel_size.y)).rgb);
925
926"#);
927
928 if self.config.variance_clipping {
929 s.push_str(&format!(r#"
930 vec3 mean = (s0+s1+s2+s3+s4) / 5.0;
931 vec3 sq_mean = (s0*s0+s1*s1+s2*s2+s3*s3+s4*s4) / 5.0;
932 vec3 variance = sq_mean - mean*mean;
933 vec3 sigma = sqrt(max(variance, vec3(0))) * {:.2};
934 vec3 hist_ycocg = rgb_to_ycocg(history);
935 hist_ycocg = clamp(hist_ycocg, mean - sigma, mean + sigma);
936 history = ycocg_to_rgb(hist_ycocg);
937"#, self.config.variance_clip_gamma));
938 } else {
939 s.push_str(r#"
940 vec3 nmin = min(s0, min(min(s1, s2), min(s3, s4)));
941 vec3 nmax = max(s0, max(max(s1, s2), max(s3, s4)));
942 vec3 hist_ycocg = rgb_to_ycocg(history);
943 hist_ycocg = clamp(hist_ycocg, nmin, nmax);
944 history = ycocg_to_rgb(hist_ycocg);
945"#);
946 }
947
948 s.push_str(r#"
949 // Exponential blend
950 float blend = u_blend_factor;
951"#);
952
953 if self.config.motion_rejection {
954 s.push_str(&format!(r#"
955 float vel_len = length(velocity);
956 blend *= 1.0 - clamp(vel_len * {:.1}, 0.0, 0.9);
957"#, self.config.motion_rejection_strength * 100.0));
958 }
959
960 if self.config.luminance_weighting {
961 s.push_str(r#"
962 float lum_current = dot(current, vec3(0.2126, 0.7152, 0.0722));
963 float lum_history = dot(history, vec3(0.2126, 0.7152, 0.0722));
964 float lum_diff = abs(lum_current - lum_history) / max(lum_current, max(lum_history, 0.001));
965 blend *= 1.0 - lum_diff * 0.5;
966"#);
967 }
968
969 s.push_str(r#"
970 vec3 result = mix(current, history, clamp(blend, 0.0, 0.98));
971 frag_color = vec4(result, 1.0);
972}
973"#);
974
975 s
976 }
977}
978
979impl Default for TaaPass {
980 fn default() -> Self {
981 Self::new()
982 }
983}
984
985#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
991pub enum MsaaSampleCount {
992 X2,
994 X4,
996 X8,
998}
999
1000impl MsaaSampleCount {
1001 pub fn count(&self) -> u32 {
1003 match self {
1004 Self::X2 => 2,
1005 Self::X4 => 4,
1006 Self::X8 => 8,
1007 }
1008 }
1009
1010 pub fn sample_positions(&self) -> Vec<[f32; 2]> {
1013 match self {
1014 Self::X2 => vec![
1015 [-0.25, -0.25],
1016 [ 0.25, 0.25],
1017 ],
1018 Self::X4 => vec![
1019 [-0.375, -0.125],
1020 [ 0.125, -0.375],
1021 [-0.125, 0.375],
1022 [ 0.375, 0.125],
1023 ],
1024 Self::X8 => vec![
1025 [-0.375, -0.375],
1026 [ 0.125, -0.375],
1027 [-0.375, -0.125],
1028 [ 0.375, -0.125],
1029 [-0.125, 0.125],
1030 [ 0.375, 0.125],
1031 [-0.125, 0.375],
1032 [ 0.125, 0.375],
1033 ],
1034 }
1035 }
1036
1037 pub fn memory_multiplier(&self) -> f32 {
1039 self.count() as f32
1040 }
1041}
1042
1043impl fmt::Display for MsaaSampleCount {
1044 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1045 write!(f, "{}x MSAA", self.count())
1046 }
1047}
1048
1049#[derive(Debug, Clone)]
1051pub struct MsaaConfig {
1052 pub enabled: bool,
1054 pub sample_count: MsaaSampleCount,
1056 pub alpha_to_coverage: bool,
1058 pub sample_shading: bool,
1060 pub min_sample_shading: f32,
1062 pub centroid_interpolation: bool,
1064 pub needs_resolve: bool,
1066 pub resolve_filter: MsaaResolveFilter,
1068}
1069
1070#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1072pub enum MsaaResolveFilter {
1073 Box,
1075 Tent,
1077 CatmullRom,
1079 BlackmanHarris,
1081}
1082
1083impl MsaaConfig {
1084 pub fn new(sample_count: MsaaSampleCount) -> Self {
1085 Self {
1086 enabled: true,
1087 sample_count,
1088 alpha_to_coverage: false,
1089 sample_shading: false,
1090 min_sample_shading: 1.0,
1091 centroid_interpolation: true,
1092 needs_resolve: true,
1093 resolve_filter: MsaaResolveFilter::Box,
1094 }
1095 }
1096
1097 pub fn memory_multiplier(&self) -> f32 {
1099 if self.enabled {
1100 self.sample_count.memory_multiplier()
1101 } else {
1102 1.0
1103 }
1104 }
1105
1106 pub fn performance_cost(&self) -> f32 {
1108 if !self.enabled {
1109 return 1.0;
1110 }
1111 let base = self.sample_count.count() as f32;
1112 if self.sample_shading {
1113 base } else {
1115 1.0 + (base - 1.0) * 0.3 }
1117 }
1118}
1119
1120impl Default for MsaaConfig {
1121 fn default() -> Self {
1122 Self::new(MsaaSampleCount::X4)
1123 }
1124}
1125
1126#[derive(Debug, Clone)]
1132pub struct CasConfig {
1133 pub sharpness: f32,
1135 pub apply_before_tonemapping: bool,
1137 pub limit_low_contrast: bool,
1139 pub denoise: f32,
1141}
1142
1143impl CasConfig {
1144 pub fn new(sharpness: f32) -> Self {
1145 Self {
1146 sharpness: clampf(sharpness, 0.0, 1.0),
1147 apply_before_tonemapping: false,
1148 limit_low_contrast: true,
1149 denoise: 0.1,
1150 }
1151 }
1152}
1153
1154impl Default for CasConfig {
1155 fn default() -> Self {
1156 Self::new(0.5)
1157 }
1158}
1159
1160#[derive(Debug)]
1162pub struct SharpeningPass {
1163 pub enabled: bool,
1165 pub config: CasConfig,
1167 pub shader_handle: u64,
1169 pub time_us: u64,
1171}
1172
1173impl SharpeningPass {
1174 pub fn new() -> Self {
1175 Self {
1176 enabled: false,
1177 config: CasConfig::default(),
1178 shader_handle: 0,
1179 time_us: 0,
1180 }
1181 }
1182
1183 pub fn with_sharpness(mut self, sharpness: f32) -> Self {
1184 self.config.sharpness = clampf(sharpness, 0.0, 1.0);
1185 self
1186 }
1187
1188 pub fn sharpen_pixel<F>(
1190 &self,
1191 x: u32,
1192 y: u32,
1193 sample: F,
1194 ) -> [f32; 3]
1195 where
1196 F: Fn(i32, i32) -> [f32; 3],
1197 {
1198 let c = sample(x as i32, y as i32);
1199 if !self.enabled || self.config.sharpness < 0.001 {
1200 return c;
1201 }
1202
1203 let nb_n = sample(x as i32, y as i32 - 1);
1205 let nb_s = sample(x as i32, y as i32 + 1);
1206 let nb_e = sample(x as i32 + 1, y as i32);
1207 let nb_w = sample(x as i32 - 1, y as i32);
1208
1209 let mut c_min = [f32::MAX; 3];
1211 let mut c_max = [f32::MIN; 3];
1212 for pixel in &[c, nb_n, nb_s, nb_e, nb_w] {
1213 for i in 0..3 {
1214 c_min[i] = c_min[i].min(pixel[i]);
1215 c_max[i] = c_max[i].max(pixel[i]);
1216 }
1217 }
1218
1219 let sharp = self.config.sharpness;
1221 let mut result = [0.0f32; 3];
1222 for i in 0..3 {
1223 let range = c_max[i] - c_min[i];
1224 let wt = if range < 1e-6 {
1225 0.0
1226 } else {
1227 let rcpmax = 1.0 / c_max[i].max(1e-6);
1228 let peak = -1.0 / (range * rcpmax * 4.0 + (1.0 - sharp));
1229 saturate(peak)
1230 };
1231
1232 let sum = nb_n[i] + nb_s[i] + nb_e[i] + nb_w[i];
1234 let sharpened = (c[i] + sum * wt) / (1.0 + 4.0 * wt);
1235 result[i] = clampf(sharpened, c_min[i], c_max[i]);
1236 }
1237
1238 result
1239 }
1240
1241 pub fn execute(&mut self, _viewport: &Viewport) {
1243 let start = std::time::Instant::now();
1244
1245 if !self.enabled {
1246 self.time_us = 0;
1247 return;
1248 }
1249
1250 self.time_us = start.elapsed().as_micros() as u64;
1257 }
1258
1259 pub fn fragment_shader(&self) -> String {
1261 format!(r#"#version 330 core
1262in vec2 v_texcoord;
1263out vec4 frag_color;
1264
1265uniform sampler2D u_scene;
1266uniform vec2 u_texel_size;
1267uniform float u_sharpness;
1268
1269void main() {{
1270 vec3 c = texture(u_scene, v_texcoord).rgb;
1271 vec3 n = texture(u_scene, v_texcoord + vec2(0.0, -u_texel_size.y)).rgb;
1272 vec3 s = texture(u_scene, v_texcoord + vec2(0.0, u_texel_size.y)).rgb;
1273 vec3 e = texture(u_scene, v_texcoord + vec2( u_texel_size.x, 0.0)).rgb;
1274 vec3 w = texture(u_scene, v_texcoord + vec2(-u_texel_size.x, 0.0)).rgb;
1275
1276 vec3 cMin = min(c, min(min(n, s), min(e, w)));
1277 vec3 cMax = max(c, max(max(n, s), max(e, w)));
1278
1279 // Adaptive sharpening weight
1280 vec3 range = cMax - cMin;
1281 vec3 rcpMax = 1.0 / max(cMax, vec3(0.0001));
1282 vec3 peak = -1.0 / (range * rcpMax * 4.0 + (1.0 - {sharpness:.4}));
1283 vec3 wt = clamp(peak, vec3(0.0), vec3(1.0));
1284
1285 vec3 result = (c + (n + s + e + w) * wt) / (1.0 + 4.0 * wt);
1286 result = clamp(result, cMin, cMax);
1287
1288 frag_color = vec4(result, 1.0);
1289}}
1290"#, sharpness = self.config.sharpness)
1291 }
1292}
1293
1294impl Default for SharpeningPass {
1295 fn default() -> Self {
1296 Self::new()
1297 }
1298}
1299
1300#[cfg(test)]
1305mod tests {
1306 use super::*;
1307
1308 #[test]
1309 fn test_aa_mode_cycling() {
1310 let mut mode = AntiAliasingMode::None;
1311 mode = mode.next();
1312 assert_eq!(mode, AntiAliasingMode::Fxaa);
1313 mode = mode.next();
1314 assert_eq!(mode, AntiAliasingMode::Taa);
1315 mode = mode.next();
1316 assert_eq!(mode, AntiAliasingMode::Msaa);
1317 mode = mode.next();
1318 assert_eq!(mode, AntiAliasingMode::FxaaPlusTaa);
1319 mode = mode.next();
1320 assert_eq!(mode, AntiAliasingMode::None);
1321 }
1322
1323 #[test]
1324 fn test_fxaa_luminance() {
1325 assert!((FxaaPass::luminance(1.0, 1.0, 1.0) - 1.0).abs() < 0.01);
1326 assert!((FxaaPass::luminance(0.0, 0.0, 0.0) - 0.0).abs() < 0.01);
1327 }
1328
1329 #[test]
1330 fn test_fxaa_quality_presets() {
1331 assert!(FxaaQuality::Low.search_steps() < FxaaQuality::Ultra.search_steps());
1332 assert!(FxaaQuality::Low.edge_threshold() > FxaaQuality::Ultra.edge_threshold());
1333 }
1334
1335 #[test]
1336 fn test_fxaa_no_edge() {
1337 let fxaa = FxaaPass::new();
1338 let result = fxaa.process_pixel(5, 5, 10, 10, |_x, _y| [0.5, 0.5, 0.5]);
1340 assert!((result[0] - 0.5).abs() < 0.01);
1341 }
1342
1343 #[test]
1344 fn test_halton_sequence() {
1345 let h0 = halton(1, 2);
1346 assert!((h0 - 0.5).abs() < 0.01);
1347 let h1 = halton(2, 2);
1348 assert!((h1 - 0.25).abs() < 0.01);
1349 }
1350
1351 #[test]
1352 fn test_jitter_sequences() {
1353 for seq in &[JitterSequence::Halton23, JitterSequence::RotatedGrid8,
1354 JitterSequence::Halton16, JitterSequence::BlueNoise] {
1355 for i in 0..seq.length() {
1356 let [x, y] = seq.sample(i);
1357 assert!(x >= -0.5 && x <= 0.5, "Jitter x out of range: {}", x);
1358 assert!(y >= -0.5 && y <= 0.5, "Jitter y out of range: {}", y);
1359 }
1360 }
1361 }
1362
1363 #[test]
1364 fn test_taa_jitter_ndc() {
1365 let mut taa = TaaPass::new();
1366 let vp = Viewport::new(1920, 1080);
1367 taa.begin_frame(&super::super::Mat4::IDENTITY);
1368 let ndc = taa.jitter_ndc(&vp);
1369 assert!(ndc[0].abs() < 0.01); assert!(ndc[1].abs() < 0.01);
1371 }
1372
1373 #[test]
1374 fn test_taa_jittered_projection() {
1375 let taa = TaaPass::new();
1376 let proj = super::super::Mat4::IDENTITY;
1377 let vp = Viewport::new(1920, 1080);
1378 let jittered = taa.jittered_projection(&proj, &vp);
1379 let _ = jittered;
1381 }
1382
1383 #[test]
1384 fn test_neighborhood_clamp() {
1385 let result = TaaPass::neighborhood_clamp(
1386 [0.5, 0.5, 0.5],
1387 [2.0, 0.0, 0.5],
1388 [0.3, 0.3, 0.3],
1389 [0.7, 0.7, 0.7],
1390 1.0,
1391 );
1392 assert!(result[0] <= 0.7);
1393 assert!(result[1] >= 0.3);
1394 }
1395
1396 #[test]
1397 fn test_variance_clip() {
1398 let result = TaaPass::variance_clip(
1399 [5.0, -1.0, 0.5],
1400 [0.5, 0.5, 0.5],
1401 [0.01, 0.01, 0.01],
1402 1.0,
1403 );
1404 assert!((result[0] - 0.6).abs() < 0.01);
1405 }
1406
1407 #[test]
1408 fn test_taa_blend_factor() {
1409 let taa = TaaPass::new();
1410 let blend = taa.compute_blend_factor(0.0);
1412 assert_eq!(blend, 0.0); let mut taa2 = TaaPass::new();
1415 taa2.history_valid = true;
1416 let blend = taa2.compute_blend_factor(0.0);
1417 assert!((blend - 0.9).abs() < 0.01);
1418
1419 let blend_fast = taa2.compute_blend_factor(0.1);
1421 assert!(blend_fast < blend);
1422 }
1423
1424 #[test]
1425 fn test_msaa_sample_count() {
1426 assert_eq!(MsaaSampleCount::X2.count(), 2);
1427 assert_eq!(MsaaSampleCount::X4.count(), 4);
1428 assert_eq!(MsaaSampleCount::X8.count(), 8);
1429 }
1430
1431 #[test]
1432 fn test_msaa_sample_positions() {
1433 let positions = MsaaSampleCount::X4.sample_positions();
1434 assert_eq!(positions.len(), 4);
1435 for pos in &positions {
1436 assert!(pos[0] >= -0.5 && pos[0] <= 0.5);
1437 assert!(pos[1] >= -0.5 && pos[1] <= 0.5);
1438 }
1439 }
1440
1441 #[test]
1442 fn test_msaa_memory() {
1443 let config = MsaaConfig::new(MsaaSampleCount::X4);
1444 assert_eq!(config.memory_multiplier(), 4.0);
1445 }
1446
1447 #[test]
1448 fn test_cas_no_sharpen() {
1449 let pass = SharpeningPass::new();
1450 let result = pass.sharpen_pixel(5, 5, |_x, _y| [0.5, 0.3, 0.7]);
1452 assert!((result[0] - 0.5).abs() < 0.01);
1453 }
1454
1455 #[test]
1456 fn test_cas_sharpen_uniform() {
1457 let mut pass = SharpeningPass::new();
1458 pass.enabled = true;
1459 pass.config.sharpness = 0.5;
1460 let result = pass.sharpen_pixel(5, 5, |_x, _y| [0.5, 0.5, 0.5]);
1462 assert!((result[0] - 0.5).abs() < 0.01);
1463 }
1464
1465 #[test]
1466 fn test_cas_config() {
1467 let config = CasConfig::new(0.8);
1468 assert!((config.sharpness - 0.8).abs() < 0.01);
1469
1470 let clamped = CasConfig::new(2.0);
1471 assert!((clamped.sharpness - 1.0).abs() < 0.01);
1472 }
1473
1474 #[test]
1475 fn test_fxaa_shader_generation() {
1476 let fxaa = FxaaPass::new().with_quality(FxaaQuality::Low);
1477 let shader = fxaa.fragment_shader();
1478 assert!(shader.contains("#version 330 core"));
1479 assert!(shader.contains("luma"));
1480 }
1481
1482 #[test]
1483 fn test_taa_shader_generation() {
1484 let taa = TaaPass::new();
1485 let shader = taa.fragment_shader();
1486 assert!(shader.contains("#version 330 core"));
1487 assert!(shader.contains("u_history"));
1488 }
1489
1490 #[test]
1491 fn test_cas_shader_generation() {
1492 let pass = SharpeningPass::new().with_sharpness(0.75);
1493 let shader = pass.fragment_shader();
1494 assert!(shader.contains("#version 330 core"));
1495 assert!(shader.contains("u_sharpness"));
1496 }
1497
1498 #[test]
1499 fn test_taa_invalidate_history() {
1500 let mut taa = TaaPass::new();
1501 taa.history_valid = true;
1502 taa.invalidate_history();
1503 assert!(!taa.history_valid);
1504 }
1505
1506 #[test]
1507 fn test_taa_config_presets() {
1508 let hq = TaaConfig::high_quality();
1509 let fast = TaaConfig::fast();
1510 assert!(hq.history_blend > fast.history_blend);
1511 assert!(hq.variance_clipping);
1512 assert!(!fast.variance_clipping);
1513 }
1514}