use crate::shader_values::{Sealed, ShaderValues};
const FRAME: &str = include_str!("renderer/post_effect.wgsl");
const SEAM: &str = "// mirage-engine:effect";
pub(crate) const GROUP: u32 = 0;
const UNPAINTED: &str = "fn draw(pixel: Pixel) -> vec4<f32> {\n return pixel.color;\n}\n";
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum EffectStage {
Lit,
ToneMapped,
OverUi,
}
impl EffectStage {
pub(crate) const ALL: [Self; 3] = [Self::Lit, Self::ToneMapped, Self::OverUi];
pub(crate) fn after_tone_map(self) -> bool {
!matches!(self, Self::Lit)
}
}
pub trait PostEffect: ShaderValues {
const STAGE: EffectStage;
const SHADER: &'static str;
}
pub trait PostEffects: Sealed + 'static {
#[doc(hidden)]
fn declared(into: &mut Declarations);
#[doc(hidden)]
fn seat(&self) -> u32;
#[doc(hidden)]
fn write(&self, into: &mut Vec<u8>);
}
#[doc(hidden)]
#[derive(Default)]
pub struct Declarations(Vec<Declaration>);
impl Declarations {
#[doc(hidden)]
pub fn declare<S: PostEffect>(&mut self) {
self.0.push(Declaration::of::<S>());
}
pub(crate) fn of<S: PostEffects>() -> Vec<Declaration> {
let mut declared = Self::default();
S::declared(&mut declared);
declared.0
}
}
#[derive(Debug)]
pub(crate) struct Declaration {
pub(crate) name: &'static str,
pub(crate) stage: EffectStage,
pub(crate) source: String,
}
impl Declaration {
fn of<S: PostEffect>() -> Self {
Self {
name: core::any::type_name::<S>(),
stage: S::STAGE,
source: stitched(&S::bound(GROUP, "effect"), S::SHADER),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub(crate) struct PostEffectId(pub(crate) u32);
impl PostEffects for () {
fn declared(_into: &mut Declarations) {}
fn seat(&self) -> u32 {
0
}
fn write(&self, _into: &mut Vec<u8>) {}
}
#[macro_export]
macro_rules! post_effects {
($(#[$attribute:meta])* $vis:vis enum $set:ident { $($effect:ident),+ $(,)? }) => {
$(#[$attribute])*
$vis enum $set {
$($effect($effect)),+
}
$(
impl ::core::convert::From<$effect> for $set {
fn from(effect: $effect) -> Self {
Self::$effect(effect)
}
}
impl $crate::Holds<$effect> for $set {}
)+
impl $crate::Sealed for $set {}
impl $crate::PostEffects for $set {
fn declared(into: &mut $crate::PostEffectDeclarations) {
$(into.declare::<$effect>();)+
}
fn seat(&self) -> u32 {
let mut at = 0;
$(
if ::core::matches!(self, Self::$effect(_)) {
return at;
}
at += 1;
)+
at
}
fn write(&self, into: &mut ::std::vec::Vec<u8>) {
match self {
$(Self::$effect(values) => $crate::ShaderValues::write(values, into)),+
}
}
}
};
}
pub(crate) fn unpainted() -> String {
stitched("", UNPAINTED)
}
fn stitched(values: &str, draw: &str) -> String {
let mut code = String::from(values);
code.push_str(draw);
FRAME.replace(SEAM, &code)
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(crate::ShaderValues)]
struct Vignette {
strength: f32,
}
impl PostEffect for Vignette {
const STAGE: EffectStage = EffectStage::ToneMapped;
const SHADER: &'static str =
"fn draw(pixel: Pixel) -> vec4<f32> { return pixel.color * effect.strength; }";
}
#[derive(crate::ShaderValues)]
struct Fog;
impl PostEffect for Fog {
const STAGE: EffectStage = EffectStage::Lit;
const SHADER: &'static str =
"fn draw(pixel: Pixel) -> vec4<f32> { return vec4<f32>(depth_at(pixel.uv)); }";
}
post_effects! { enum Look { Fog, Vignette } }
#[test]
fn the_shader_carries_one_seam_for_an_effect_to_be_stitched_into() {
assert_eq!(FRAME.matches(SEAM).count(), 1);
}
#[test]
fn an_effects_own_code_is_called_from_the_seam_and_its_values_are_bound() {
let source = Declaration::of::<Vignette>().source;
assert!(!source.contains(SEAM), "the seam itself is replaced");
assert!(source.contains("struct Vignette"));
assert!(source.contains("@group(0) @binding(0) var<uniform> effect: Vignette;"));
assert!(
source.find("struct Vignette") < source.find("fn draw(pixel: Pixel)"),
"and the values are declared before the code that reads them"
);
}
#[test]
fn an_effect_with_no_fields_reads_no_values_and_binds_none() {
let source = Declaration::of::<Fog>().source;
assert!(!source.contains("@group(0)"));
assert!(source.contains("fn draw(pixel: Pixel)"));
}
#[test]
fn a_set_declares_its_effects_in_the_order_it_names_them() {
let declared = Declarations::of::<Look>();
assert_eq!(
declared
.iter()
.map(|effect| effect.stage)
.collect::<Vec<_>>(),
vec![EffectStage::Lit, EffectStage::ToneMapped]
);
assert_eq!(
(
Look::from(Fog).seat(),
Look::from(Vignette { strength: 0.0 }).seat()
),
(0, 1)
);
assert!(Declarations::of::<()>().is_empty());
}
#[test]
fn a_set_value_lays_out_the_values_of_the_effect_it_holds() {
let mut written = Vec::new();
Look::from(Vignette { strength: 0.25 }).write(&mut written);
assert_eq!(
f32::from_le_bytes(written[..4].try_into().expect("four bytes")),
0.25
);
let mut none = Vec::new();
Look::from(Fog).write(&mut none);
assert!(none.is_empty(), "where an effect reads nothing");
}
#[test]
fn the_places_run_the_scene_first_and_what_is_over_the_ui_last() {
assert_eq!(
EffectStage::ALL,
[
EffectStage::Lit,
EffectStage::ToneMapped,
EffectStage::OverUi
]
);
assert!(
EffectStage::Lit < EffectStage::ToneMapped
&& EffectStage::ToneMapped < EffectStage::OverUi
);
assert_eq!(
EffectStage::ALL.map(EffectStage::after_tone_map),
[false, true, true],
"and only the first draws in the scene's own light"
);
}
}