Skip to main content

fullscreen_material/
fullscreen_material.rs

1//! Demonstrates how to write a custom fullscreen shader
2//!
3//! This example demonstrates working in 3d. To make the example work in 2d,
4//! replace 3d components with their 2d counterparts, and schedule the work
5//! to run in the `Core2d` schedule as described in the `FullscreenMaterial`
6//! comment in this file.
7
8use bevy::{
9    core_pipeline::fullscreen_material::{FullscreenMaterial, FullscreenMaterialPlugin},
10    prelude::*,
11    render::{extract_component::ExtractComponent, render_resource::ShaderType},
12    shader::ShaderRef,
13};
14
15fn main() {
16    App::new()
17        .add_plugins((
18            DefaultPlugins,
19            FullscreenMaterialPlugin::<FullscreenEffect>::default(),
20        ))
21        .add_systems(Startup, setup)
22        .add_systems(Update, (update_intensity, toggle_effect))
23        .run();
24}
25
26fn setup(
27    mut commands: Commands,
28    mut meshes: ResMut<Assets<Mesh>>,
29    mut materials: ResMut<Assets<StandardMaterial>>,
30) {
31    commands.spawn((
32        Camera3d::default(),
33        Transform::from_translation(Vec3::new(0.0, 0.0, 5.0)).looking_at(Vec3::default(), Vec3::Y),
34        FullscreenEffect::new(0.0),
35    ));
36
37    commands.spawn((
38        Mesh3d(meshes.add(Cuboid::default())),
39        MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))),
40        Transform::default(),
41    ));
42
43    commands.spawn(DirectionalLight {
44        illuminance: 1_000.,
45        ..default()
46    });
47
48    commands.spawn((
49        Text::new("(T) FullscreenEffect: On"),
50        Node {
51            position_type: PositionType::Absolute,
52            top: px(12),
53            left: px(12),
54            ..default()
55        },
56    ));
57}
58
59fn update_intensity(
60    mut effects: Query<&mut FullscreenEffect>,
61    time: Res<Time>,
62    mut last_intensity: Local<f32>,
63    mut phase_offset: Local<f32>,
64) {
65    let t = time.elapsed_secs();
66    let freq = FullscreenEffect::FREQUENCY;
67    let max = FullscreenEffect::MAX_INTENSITY;
68
69    for mut effect in &mut effects {
70        // Check if the intensity was modified externally since last frame.
71        // This ensures that when intensity is modified, this system recalculates
72        // the phase offset to avoid intensity jumps.
73        if effect.intensity != *last_intensity {
74            // Map the target intensity back to sine range [-1, 1]
75            let target_sine = (effect.intensity / max) * 2.0 - 1.0;
76            // Compute a phase offset so that `ops::sin(t * freq + offset) == target_sine`
77            *phase_offset = ops::asin(target_sine) - t * freq;
78        }
79
80        // Compute the new intensity from the (possibly adjusted) phase offset
81        let phase = t * freq + *phase_offset;
82        // Make it loop periodically
83        let mut intensity = ops::sin(phase);
84
85        // We need to remap the intensity to be between 0 and 1 instead of -1 and 1
86        intensity = (intensity + 1.0) / 2.0;
87        *last_intensity = intensity * max;
88
89        effect.intensity = *last_intensity;
90    }
91}
92
93fn toggle_effect(
94    mut text: Single<&mut Text>,
95    keys: Res<ButtonInput<KeyCode>>,
96    camera: Single<(Entity, Option<&FullscreenEffect>), With<Camera3d>>,
97    mut commands: Commands,
98) {
99    if keys.just_pressed(KeyCode::KeyT) {
100        let (entity, effect) = *camera;
101
102        if effect.is_some() {
103            commands.entity(entity).remove::<FullscreenEffect>();
104            text.clear();
105            text.push_str("(T) FullscreenEffect: Off");
106        } else {
107            commands.entity(entity).insert(FullscreenEffect::new(0.0));
108            text.clear();
109            text.push_str("(T) FullscreenEffect: On");
110        }
111    }
112}
113
114#[derive(Component, ExtractComponent, Clone, Copy, ShaderType, Default)]
115struct FullscreenEffect {
116    intensity: f32,
117    // WebGL2 structs must be 16 byte aligned.
118    // Intensity is an `f32`, which is 4 bytes, so 12 more bytes (3 floats) are needed.
119    #[cfg(feature = "webgl2")]
120    _webgl2_padding: Vec3,
121}
122
123impl FullscreenEffect {
124    const FREQUENCY: f32 = 2.0;
125    const MAX_INTENSITY: f32 = 0.015;
126
127    fn new(intensity: f32) -> Self {
128        Self {
129            intensity,
130            ..Default::default()
131        }
132    }
133}
134
135impl FullscreenMaterial for FullscreenEffect {
136    fn fragment_shader() -> ShaderRef {
137        "shaders/fullscreen_effect.wgsl".into()
138    }
139
140    // The `FullscreenMaterial` uses 3d schedules by default.
141    // To make this work in 2d, you would need to schedule to
142    // run in `Core2d` and in a `Core2dSystems` set.
143    //
144    // fn schedule() -> impl bevy::ecs::schedule::ScheduleLabel + Clone {
145    //     bevy::core_pipeline::Core2d
146    // }
147    // fn schedule_configs(
148    //     system: bevy::ecs::schedule::ScheduleConfigs<bevy::ecs::system::BoxedSystem>,
149    // ) -> bevy::ecs::schedule::ScheduleConfigs<bevy::ecs::system::BoxedSystem> {
150    //     system
151    //         .in_set(bevy::core_pipeline::Core2dSystems::PostProcess)
152    //         .before(bevy::core_pipeline::tonemapping::tonemapping)
153    // }
154}