docs/bevy/examples/shader_advanced/fullscreen_material.rs 4.9 K · 155 lines · raw · history

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
8 use bevy::{
9 core_pipeline::fullscreen_material::{FullscreenMaterial, FullscreenMaterialPlugin},
10 prelude::*,
11 render::{extract_component::ExtractComponent, render_resource::ShaderType, RenderApp},
12 shader::ShaderRef,
13 };
14
15 fn 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
26 fn 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
59 fn 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
93 fn 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)]
115 #[extract_app(RenderApp)]
116 struct FullscreenEffect {
117 intensity: f32,
118 // WebGL2 structs must be 16 byte aligned.
119 // Intensity is an `f32`, which is 4 bytes, so 12 more bytes (3 floats) are needed.
120 #[cfg(feature = "webgl2")]
121 _webgl2_padding: Vec3,
122 }
123
124 impl FullscreenEffect {
125 const FREQUENCY: f32 = 2.0;
126 const MAX_INTENSITY: f32 = 0.015;
127
128 fn new(intensity: f32) -> Self {
129 Self {
130 intensity,
131 ..Default::default()
132 }
133 }
134 }
135
136 impl FullscreenMaterial for FullscreenEffect {
137 fn fragment_shader() -> ShaderRef {
138 "shaders/fullscreen_effect.wesl".into()
139 }
140
141 // The `FullscreenMaterial` uses 3d schedules by default.
142 // To make this work in 2d, you would need to schedule to
143 // run in `Core2d` and in a `Core2dSystems` set.
144 //
145 // fn schedule() -> impl bevy::ecs::schedule::ScheduleLabel + Clone {
146 // bevy::core_pipeline::Core2d
147 // }
148 // fn schedule_configs(
149 // system: bevy::ecs::schedule::ScheduleConfigs<bevy::ecs::system::BoxedSystem>,
150 // ) -> bevy::ecs::schedule::ScheduleConfigs<bevy::ecs::system::BoxedSystem> {
151 // system
152 // .in_set(bevy::core_pipeline::Core2dSystems::PostProcess)
153 // .before(bevy::core_pipeline::tonemapping::tonemapping)
154 // }
155 }