Skip to main content

specular_tint/
specular_tint.rs

1//! Demonstrates specular tints and maps.
2
3use 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
25/// The camera rotation speed in radians per frame.
26const ROTATION_SPEED: f32 = 0.005;
27/// The rate at which the specular tint hue changes in degrees per frame.
28const HUE_SHIFT_SPEED: f32 = 0.2;
29
30/// The current settings the user has chosen.
31#[derive(Resource, Default)]
32struct 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)]
41struct AppAssets {
42    /// A color tileable 3D noise texture.
43    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/// The type of specular tint that the user has selected.
56#[derive(Clone, Copy, PartialEq, Default, Debug)]
57enum TintType {
58    /// A solid color.
59    #[default]
60    Solid,
61    /// A Perlin noise texture.
62    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
74/// The entry point.
75fn 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.
101fn 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.
152fn 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.
161fn 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.
181fn 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.
199fn 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}