mod bloom;
mod blur;
mod chromatic_aberration;
mod color_matrix;
use bevy::prelude::*;
pub use bloom::BloomParams;
pub use blur::BlurParams;
pub use chromatic_aberration::ChromaticAberrationParams;
pub use color_matrix::{
BrightnessParams, ContrastParams, GrayscaleParams, HueRotateParams, InvertParams,
SaturateParams, SepiaParams,
};
use super::registry::FilterRegistry;
impl FilterRegistry {
pub(crate) fn register_builtins(&mut self) {
self.register::<BloomParams>();
self.register::<BlurParams>();
self.register::<ChromaticAberrationParams>();
self.register::<BrightnessParams>();
self.register::<ContrastParams>();
self.register::<SaturateParams>();
self.register::<GrayscaleParams>();
self.register::<SepiaParams>();
self.register::<InvertParams>();
self.register::<HueRotateParams>();
}
}
pub fn register_builtin_filters(app: &mut App) {
app.world_mut()
.get_resource_or_init::<FilterRegistry>()
.register_builtins();
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
use crate::filters::params::MAX_FILTER_PARAM_VECS;
use crate::filters::registry::ReactFilter;
use crate::filters::test_util::{asset_app, builtin_registry, params};
use crate::protocol::Length;
#[test]
fn empty_params_default_to_css_shorthand_values() {
assert_eq!(params::<BrightnessParams>(json!({})).amount, 1.0);
assert_eq!(params::<ContrastParams>(json!({})).amount, 1.0);
assert_eq!(params::<SaturateParams>(json!({})).amount, 1.0);
assert_eq!(params::<SepiaParams>(json!({})).amount, 1.0);
assert_eq!(params::<InvertParams>(json!({})).amount, 1.0);
let g = params::<GrayscaleParams>(json!({}));
assert_eq!(g.amount, 1.0);
assert_eq!(g.pack().0[0].w, 1.0);
assert_eq!(params::<BlurParams>(json!({})).radius, Length::Px(0.0));
assert_eq!(params::<HueRotateParams>(json!({})).angle.radians(), 0.0);
let b = params::<BloomParams>(json!({}));
assert_eq!(b.radius, Length::Px(12.0));
assert_eq!(b.threshold, 0.7);
assert_eq!(b.intensity, 1.0);
let ca = params::<ChromaticAberrationParams>(json!({}));
assert_eq!(ca.offset, Length::Px(4.0));
assert_eq!(ca.angle.radians(), 0.0);
}
#[test]
fn unknown_param_key_is_rejected() {
assert!(serde_json::from_value::<BlurParams>(json!({ "radius": 4, "bogus": 1 })).is_err());
let app = asset_app();
let assets = app.world().resource::<AssetServer>();
let mut registry = FilterRegistry::default();
registry.register::<GrayscaleParams>();
let err = (registry.entries["grayscale"].resolve)(&json!({ "typo": 2 }), assets)
.expect_err("unknown key must reject");
assert!(err.contains("grayscale"), "error names the filter: {err}");
}
#[test]
fn builtin_identity_params_are_true_identities() {
let r = builtin_registry();
let amount = |name: &str| (r.entries[name].identity)().unwrap()["amount"].clone();
assert_eq!(amount("brightness"), json!(1.0));
assert_eq!(amount("contrast"), json!(1.0));
assert_eq!(amount("saturate"), json!(1.0));
assert_eq!(amount("grayscale"), json!(0.0));
assert_eq!(amount("sepia"), json!(0.0));
assert_eq!(amount("invert"), json!(0.0));
assert_eq!(
(r.entries["blur"].identity)().unwrap()["radius"],
json!(0.0)
);
assert_eq!(
(r.entries["hueRotate"].identity)().unwrap()["angle"],
json!(0.0)
);
assert_eq!(
(r.entries["bloom"].identity)().unwrap()["intensity"],
json!(0.0)
);
assert_eq!(
(r.entries["chromaticAberration"].identity)().unwrap()["offset"],
json!(0.0)
);
let app = asset_app();
let assets = app.world().resource::<AssetServer>();
let passes =
(r.entries["grayscale"].resolve)(&(r.entries["grayscale"].identity)().unwrap(), assets)
.expect("identity resolves");
assert_eq!(passes[0].params[0].w, 0.0, "identity packing, no effect");
let passes =
(r.entries["bloom"].resolve)(&(r.entries["bloom"].identity)().unwrap(), assets)
.expect("identity resolves");
assert_eq!(passes.len(), 4);
assert!(passes.iter().all(|p| p.params[1].y == 0.0));
}
#[test]
fn filter_wgsl_parses_and_validates() {
fn validate(name: &str, source: &str, entry_points: &[&str]) {
let module = naga::front::wgsl::parse_str(source)
.unwrap_or_else(|e| panic!("{name} does not parse:\n{}", e.emit_to_string(source)));
naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
)
.validate(&module)
.unwrap_or_else(|e| panic!("{name} does not validate: {e:?}"));
for entry in entry_points {
assert!(
module.entry_points.iter().any(|e| e.name == *entry),
"{name} is missing entry point `{entry}` — splice mangled?"
);
}
}
fn splice(prelude_body: &str, src: &str) -> String {
let mut out = String::new();
let mut lines = src.lines();
while let Some(line) = lines.next() {
if line.trim_start().starts_with("#import") {
out.push_str(prelude_body);
out.push('\n');
if !line.contains('}') {
for rest in lines.by_ref() {
if rest.trim() == "}" {
break;
}
}
}
} else {
out.push_str(line);
out.push('\n');
}
}
out
}
let prelude_body: String = include_str!("../../layer/filter_prelude.wgsl")
.lines()
.filter(|l| !l.trim_start().starts_with("#define_import_path"))
.collect::<Vec<_>>()
.join("\n");
validate("filter_prelude.wgsl", &prelude_body, &["vertex"]);
validate(
"color_matrix.wgsl",
&splice(&prelude_body, include_str!("color_matrix.wgsl")),
&["vertex", "fragment"],
);
validate(
"blur.wgsl",
&splice(&prelude_body, include_str!("blur.wgsl")),
&["vertex", "fragment"],
);
validate(
"bloom.wgsl",
&splice(&prelude_body, include_str!("bloom.wgsl")),
&["vertex", "fragment"],
);
validate(
"chromatic_aberration.wgsl",
&splice(&prelude_body, include_str!("chromatic_aberration.wgsl")),
&["vertex", "fragment"],
);
assert!(
include_str!("../../layer/filter_prelude.wgsl")
.contains(&format!("array<vec4<f32>, {MAX_FILTER_PARAM_VECS}>")),
"filter_prelude.wgsl params array size must equal MAX_FILTER_PARAM_VECS"
);
}
}