use bevy::asset::RenderAssetUsages;
use bevy::image::Image;
use bevy::platform::collections::HashMap;
use bevy::prelude::*;
use bevy::render::render_resource::{AsBindGroup, Extent3d, TextureDimension, TextureFormat};
use bevy::shader::ShaderRef;
use crate::protocol::{Angle, FilterSpec};
#[derive(Asset, AsBindGroup, Reflect, Clone, Default)]
pub struct FilterMaterial {
#[uniform(0)]
pub base_color: Vec4,
#[uniform(1)]
pub color: Vec4,
#[uniform(2)]
pub color2: Vec4,
#[uniform(3)]
pub blur: Vec4,
#[texture(4)]
#[sampler(5)]
pub texture: Handle<Image>,
}
impl UiMaterial for FilterMaterial {
fn fragment_shader() -> ShaderRef {
"embedded://bevy_react/filter.wgsl".into()
}
}
pub fn filter_material(spec: &FilterSpec, texture: Handle<Image>, base: Color) -> FilterMaterial {
let base_color = Vec4::from_array(base.to_linear().to_f32_array());
FilterMaterial {
base_color,
color: Vec4::new(
spec.brightness.unwrap_or(1.0),
spec.contrast.unwrap_or(1.0),
spec.saturate.unwrap_or(1.0),
spec.grayscale.unwrap_or(0.0),
),
color2: Vec4::new(
spec.sepia.unwrap_or(0.0),
spec.invert.unwrap_or(0.0),
spec.hue_rotate.map(Angle::radians).unwrap_or(0.0),
0.0,
),
blur: Vec4::new(
spec.blur
.map(crate::ui_map::length_to_val)
.map(val_px)
.unwrap_or(0.0),
0.0,
0.0,
0.0,
),
texture,
}
}
fn val_px(v: Val) -> f32 {
match v {
Val::Px(px) => px,
_ => 0.0,
}
}
#[derive(Resource)]
pub struct FilterAssets {
pub white: Handle<Image>,
}
#[derive(Resource, Default)]
pub struct FilterMaterialCache(HashMap<FilterKey, Handle<FilterMaterial>>);
#[derive(PartialEq, Eq, Hash)]
struct FilterKey {
texture: AssetId<Image>,
bits: [u32; 16],
}
impl FilterKey {
fn of(mat: &FilterMaterial) -> Self {
let mut bits = [0u32; 16];
for (slot, v) in [mat.base_color, mat.color, mat.color2, mat.blur]
.iter()
.enumerate()
{
bits[slot * 4..slot * 4 + 4].copy_from_slice(&v.to_array().map(f32::to_bits));
}
FilterKey {
texture: mat.texture.id(),
bits,
}
}
}
impl FilterMaterialCache {
pub fn handle(
&mut self,
materials: &mut Assets<FilterMaterial>,
mat: FilterMaterial,
) -> Handle<FilterMaterial> {
self.0
.entry(FilterKey::of(&mat))
.or_insert_with(|| materials.add(mat))
.clone()
}
}
pub fn init_filter_assets(mut commands: Commands, mut images: ResMut<Assets<Image>>) {
let white = images.add(Image::new_fill(
Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
TextureDimension::D2,
&[255, 255, 255, 255],
TextureFormat::Rgba8UnormSrgb,
RenderAssetUsages::RENDER_WORLD,
));
commands.insert_resource(FilterAssets { white });
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::Style;
#[test]
fn empty_filter_is_identity() {
let spec = FilterSpec::default();
let mat = filter_material(&spec, Handle::default(), Color::WHITE);
assert_eq!(mat.color, Vec4::new(1.0, 1.0, 1.0, 0.0));
assert_eq!(mat.color2, Vec4::new(0.0, 0.0, 0.0, 0.0));
assert_eq!(mat.blur.x, 0.0);
}
#[test]
fn packs_functions_into_uniform_slots() {
let style: Style = serde_json::from_str(
r#"{ "filter": {
"blur": "4px", "brightness": 1.2, "contrast": 0.8,
"saturate": 1.5, "grayscale": 0.25, "sepia": 0.5,
"invert": 1, "hueRotate": 180
} }"#,
)
.unwrap();
let mat = filter_material(&style.filter.unwrap(), Handle::default(), Color::WHITE);
assert_eq!(mat.color, Vec4::new(1.2, 0.8, 1.5, 0.25));
assert!((mat.color2.x - 0.5).abs() < 1e-6); assert!((mat.color2.y - 1.0).abs() < 1e-6); assert!((mat.color2.z - std::f32::consts::PI).abs() < 1e-5); assert_eq!(mat.blur.x, 4.0);
}
#[test]
fn cache_dedupes_by_texture_and_params() {
let mut materials = Assets::<FilterMaterial>::default();
let mut cache = FilterMaterialCache::default();
let spec: FilterSpec = serde_json::from_str(r#"{ "grayscale": 1 }"#).unwrap();
let a = cache.handle(
&mut materials,
filter_material(&spec, Handle::default(), Color::WHITE),
);
let b = cache.handle(
&mut materials,
filter_material(&spec, Handle::default(), Color::WHITE),
);
assert_eq!(a, b, "same inputs reuse one handle");
assert_eq!(materials.len(), 1);
let other: FilterSpec = serde_json::from_str(r#"{ "grayscale": 0.5 }"#).unwrap();
let c = cache.handle(
&mut materials,
filter_material(&other, Handle::default(), Color::WHITE),
);
assert_ne!(a, c, "different params mint a new handle");
assert_eq!(materials.len(), 2);
}
}