docs/bevy/examples/3d/specular_tint.rs 7.3 K · 238 lines · raw · history

1 //! Demonstrates specular tints and maps.
2
3 use std::{
4 f32::consts::PI,
5 fmt::{self, Formatter},
6 };
7
8 use 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
17 use radio::{feathers_option_buttons, main_ui_node_scene, RadioButtonOptionValue};
18
19 #[path = "../helpers/radio.rs"]
20 mod radio;
21
22 #[path = "../helpers/theme.rs"]
23 mod theme;
24
25 /// The camera rotation speed in radians per frame.
26 const ROTATION_SPEED: f32 = 0.005;
27 /// The rate at which the specular tint hue changes in degrees per frame.
28 const HUE_SHIFT_SPEED: f32 = 0.2;
29
30 /// The current settings the user has chosen.
31 #[derive(Resource, Default)]
32 struct AppStatus {
33 /// The type of tint (solid or texture map).
34 tint_type: TintType,
35 /// The hue of the solid tint in radians.
36 hue: f32,
37 }
38
39 /// Assets needed by the demo.
40 #[derive(Resource)]
41 struct AppAssets {
42 /// A color tileable 3D noise texture.
43 noise_texture: Handle<Image>,
44 }
45
46 impl 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 /// The type of specular tint that the user has selected.
56 #[derive(Clone, Copy, PartialEq, Default, Debug)]
57 enum TintType {
58 /// A solid color.
59 #[default]
60 Solid,
61 /// A Perlin noise texture.
62 Map,
63 }
64
65 impl 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
74 /// The entry point.
75 fn 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
100 /// Creates the scene.
101 fn 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 // Spawns a camera.
109 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 // We want relatively high intensity here in order for the specular
122 // tint to show up well.
123 intensity: 25000.0,
124 ..default()
125 },
126 ));
127
128 // Spawn the sphere.
129 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 // We want only reflected specular light here, so we set the base
134 // color as black.
135 base_color: Color::BLACK,
136 reflectance: 1.0,
137 specular_tint: Color::hsva(app_status.hue, 1.0, 1.0, 1.0),
138 // The object must not be metallic, or else the reflectance is
139 // ignored per the Filament spec:
140 //
141 // <https://google.github.io/filament/Filament.md.html#listing_fnormal>
142 metallic: 0.0,
143 perceptual_roughness: 0.0,
144 ..default()
145 })),
146 ));
147
148 spawn_buttons(&mut commands);
149 }
150
151 /// Rotates the camera a bit every frame.
152 fn 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
160 /// Alters the hue of the solid color a bit every frame.
161 fn 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
180 /// Spawns the radio buttons in the bottom left corner of the screen.
181 fn 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
197 /// Changes the specular tint to a solid color or map when the user checks
198 /// a radio button.
199 fn 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 // Set the specular map value in `AppStatus`.
212 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 // Adjust the tint type.
220 match app_status.tint_type {
221 TintType::Solid => {
222 material.reflectance = 1.0;
223 material.specular_tint_texture = None;
224 }
225 TintType::Map => {
226 // Set reflectance to 2.0 to spread out the map's reflectance
227 // range from the default [0.0, 0.5] to [0.0, 1.0].
228 material.reflectance = 2.0;
229 // As the tint map is multiplied by the tint color, we set the
230 // latter to white so that only the map has an effect.
231 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 }