1use std::{
4 f32::consts::PI,
5 fmt::{self, Formatter},
6};
7
8use bevy::{
9 camera::Hdr,
10 color::palettes::css::WHITE,
11 feathers::{theme::UiTheme, FeathersPlugins},
12 light::Skybox,
13 prelude::*,
14 ui_widgets::{radio_self_update, ValueChange},
15};
16
17use radio::{feathers_option_buttons, main_ui_node_scene, RadioButtonOptionValue};
18
19#[path = "../helpers/radio.rs"]
20mod radio;
21
22#[path = "../helpers/theme.rs"]
23mod theme;
24
25const ROTATION_SPEED: f32 = 0.005;
27const HUE_SHIFT_SPEED: f32 = 0.2;
29
30#[derive(Resource, Default)]
32struct AppStatus {
33 tint_type: TintType,
35 hue: f32,
37}
38
39#[derive(Resource)]
41struct AppAssets {
42 noise_texture: Handle<Image>,
44}
45
46impl FromWorld for AppAssets {
47 fn from_world(world: &mut World) -> Self {
48 let asset_server = world.resource::<AssetServer>();
49 Self {
50 noise_texture: asset_server.load("textures/AlphaNoise.png"),
51 }
52 }
53}
54
55#[derive(Clone, Copy, PartialEq, Default, Debug)]
57enum TintType {
58 #[default]
60 Solid,
61 Map,
63}
64
65impl fmt::Display for TintType {
66 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
67 match *self {
68 TintType::Map => f.write_str("MAP"),
69 TintType::Solid => f.write_str("SOLID"),
70 }
71 }
72}
73
74fn main() {
76 App::new()
77 .add_plugins(DefaultPlugins.set(WindowPlugin {
78 primary_window: Some(Window {
79 title: "Bevy Specular Tint Example".into(),
80 ..default()
81 }),
82 ..default()
83 }))
84 .add_plugins(FeathersPlugins)
85 .insert_resource(UiTheme(theme::basic_example_theme(Color::WHITE)))
86 .init_resource::<AppAssets>()
87 .init_resource::<AppStatus>()
88 .insert_resource(GlobalAmbientLight {
89 color: Color::BLACK,
90 brightness: 0.0,
91 ..default()
92 })
93 .add_systems(Startup, setup)
94 .add_systems(Update, rotate_camera)
95 .add_observer(toggle_specular_map)
96 .add_observer(radio_self_update)
97 .run();
98}
99
100fn setup(
102 mut commands: Commands,
103 asset_server: Res<AssetServer>,
104 app_status: Res<AppStatus>,
105 mut meshes: ResMut<Assets<Mesh>>,
106 mut standard_materials: ResMut<Assets<StandardMaterial>>,
107) {
108 commands.spawn((
110 Transform::from_xyz(-2.0, 0.0, 3.5).looking_at(Vec3::ZERO, Vec3::Y),
111 Hdr,
112 Camera3d::default(),
113 Skybox {
114 image: Some(asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2")),
115 brightness: 3000.0,
116 ..default()
117 },
118 EnvironmentMapLight {
119 diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
120 specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
121 intensity: 25000.0,
124 ..default()
125 },
126 ));
127
128 commands.spawn((
130 Transform::from_rotation(Quat::from_rotation_x(PI * 0.5)),
131 Mesh3d(meshes.add(Sphere::default().mesh().uv(32, 18))),
132 MeshMaterial3d(standard_materials.add(StandardMaterial {
133 base_color: Color::BLACK,
136 reflectance: 1.0,
137 specular_tint: Color::hsva(app_status.hue, 1.0, 1.0, 1.0),
138 metallic: 0.0,
143 perceptual_roughness: 0.0,
144 ..default()
145 })),
146 ));
147
148 spawn_buttons(&mut commands);
149}
150
151fn rotate_camera(mut cameras: Query<&mut Transform, With<Camera3d>>) {
153 for mut camera_transform in cameras.iter_mut() {
154 camera_transform.translation =
155 Quat::from_rotation_y(ROTATION_SPEED) * camera_transform.translation;
156 camera_transform.look_at(Vec3::ZERO, Vec3::Y);
157 }
158}
159
160fn shift_hue(
162 mut app_status: ResMut<AppStatus>,
163 objects_with_materials: Query<&MeshMaterial3d<StandardMaterial>>,
164 mut standard_materials: ResMut<Assets<StandardMaterial>>,
165) {
166 if app_status.tint_type != TintType::Solid {
167 return;
168 }
169
170 app_status.hue += HUE_SHIFT_SPEED;
171
172 for material_handle in objects_with_materials.iter() {
173 let Some(mut material) = standard_materials.get_mut(material_handle) else {
174 continue;
175 };
176 material.specular_tint = Color::hsva(app_status.hue, 1.0, 1.0, 1.0);
177 }
178}
179
180fn spawn_buttons(commands: &mut Commands) {
182 commands.spawn_scene(bsn! {
183 @main_ui_node_scene()
184 Children [
185 @feathers_option_buttons(
186 "Toggle specular tint",
187 &[
188 (TintType::Solid, "SOLID"),
189 (TintType::Map, "MAP"),
190 ],
191 0,
192 )
193 ]
194 });
195}
196
197fn toggle_specular_map(
200 event: On<ValueChange<Entity>>,
201 new_value_query: Query<&RadioButtonOptionValue<TintType>>,
202 mut app_status: ResMut<AppStatus>,
203 app_assets: Res<AppAssets>,
204 objects_with_materials: Query<&MeshMaterial3d<StandardMaterial>>,
205 mut standard_materials: ResMut<Assets<StandardMaterial>>,
206) {
207 let Ok(RadioButtonOptionValue(selection)) = new_value_query.get(event.value) else {
208 return;
209 };
210
211 app_status.tint_type = *selection;
213
214 for material_handle in objects_with_materials.iter() {
215 let Some(mut material) = standard_materials.get_mut(material_handle) else {
216 continue;
217 };
218
219 match app_status.tint_type {
221 TintType::Solid => {
222 material.reflectance = 1.0;
223 material.specular_tint_texture = None;
224 }
225 TintType::Map => {
226 material.reflectance = 2.0;
229 material.specular_tint = WHITE.into();
232 material.specular_tint_texture = Some(app_assets.noise_texture.clone());
233 }
234 };
235 }
236
237 shift_hue(app_status, objects_with_materials, standard_materials);
238}