specular_tint/
specular_tint.rs

1//! Demonstrates specular tints and maps.
2
3use std::f32::consts::PI;
4
5use bevy::{color::palettes::css::WHITE, core_pipeline::Skybox, prelude::*, render::view::Hdr};
6
7/// The camera rotation speed in radians per frame.
8const ROTATION_SPEED: f32 = 0.005;
9/// The rate at which the specular tint hue changes in degrees per frame.
10const HUE_SHIFT_SPEED: f32 = 0.2;
11
12static SWITCH_TO_MAP_HELP_TEXT: &str = "Press Space to switch to a specular map";
13static SWITCH_TO_SOLID_TINT_HELP_TEXT: &str = "Press Space to switch to a solid specular tint";
14
15/// The current settings the user has chosen.
16#[derive(Resource, Default)]
17struct AppStatus {
18    /// The type of tint (solid or texture map).
19    tint_type: TintType,
20    /// The hue of the solid tint in radians.
21    hue: f32,
22}
23
24/// Assets needed by the demo.
25#[derive(Resource)]
26struct AppAssets {
27    /// A color tileable 3D noise texture.
28    noise_texture: Handle<Image>,
29}
30
31impl FromWorld for AppAssets {
32    fn from_world(world: &mut World) -> Self {
33        let asset_server = world.resource::<AssetServer>();
34        Self {
35            noise_texture: asset_server.load("textures/AlphaNoise.png"),
36        }
37    }
38}
39
40/// The type of specular tint that the user has selected.
41#[derive(Clone, Copy, PartialEq, Default)]
42enum TintType {
43    /// A solid color.
44    #[default]
45    Solid,
46    /// A Perlin noise texture.
47    Map,
48}
49
50/// The entry point.
51fn main() {
52    App::new()
53        .add_plugins(DefaultPlugins.set(WindowPlugin {
54            primary_window: Some(Window {
55                title: "Bevy Specular Tint Example".into(),
56                ..default()
57            }),
58            ..default()
59        }))
60        .init_resource::<AppAssets>()
61        .init_resource::<AppStatus>()
62        .insert_resource(AmbientLight {
63            color: Color::BLACK,
64            brightness: 0.0,
65            ..default()
66        })
67        .add_systems(Startup, setup)
68        .add_systems(Update, rotate_camera)
69        .add_systems(Update, (toggle_specular_map, update_text).chain())
70        .add_systems(Update, shift_hue.after(toggle_specular_map))
71        .run();
72}
73
74/// Creates the scene.
75fn setup(
76    mut commands: Commands,
77    asset_server: Res<AssetServer>,
78    app_status: Res<AppStatus>,
79    mut meshes: ResMut<Assets<Mesh>>,
80    mut standard_materials: ResMut<Assets<StandardMaterial>>,
81) {
82    // Spawns a camera.
83    commands.spawn((
84        Transform::from_xyz(-2.0, 0.0, 3.5).looking_at(Vec3::ZERO, Vec3::Y),
85        Hdr,
86        Camera3d::default(),
87        Skybox {
88            image: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
89            brightness: 3000.0,
90            ..default()
91        },
92        EnvironmentMapLight {
93            diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
94            specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
95            // We want relatively high intensity here in order for the specular
96            // tint to show up well.
97            intensity: 25000.0,
98            ..default()
99        },
100    ));
101
102    // Spawn the sphere.
103    commands.spawn((
104        Transform::from_rotation(Quat::from_rotation_x(PI * 0.5)),
105        Mesh3d(meshes.add(Sphere::default().mesh().uv(32, 18))),
106        MeshMaterial3d(standard_materials.add(StandardMaterial {
107            // We want only reflected specular light here, so we set the base
108            // color as black.
109            base_color: Color::BLACK,
110            reflectance: 1.0,
111            specular_tint: Color::hsva(app_status.hue, 1.0, 1.0, 1.0),
112            // The object must not be metallic, or else the reflectance is
113            // ignored per the Filament spec:
114            //
115            // <https://google.github.io/filament/Filament.html#listing_fnormal>
116            metallic: 0.0,
117            perceptual_roughness: 0.0,
118            ..default()
119        })),
120    ));
121
122    // Spawn the help text.
123    commands.spawn((
124        Node {
125            position_type: PositionType::Absolute,
126            bottom: px(12),
127            left: px(12),
128            ..default()
129        },
130        app_status.create_text(),
131    ));
132}
133
134/// Rotates the camera a bit every frame.
135fn rotate_camera(mut cameras: Query<&mut Transform, With<Camera3d>>) {
136    for mut camera_transform in cameras.iter_mut() {
137        camera_transform.translation =
138            Quat::from_rotation_y(ROTATION_SPEED) * camera_transform.translation;
139        camera_transform.look_at(Vec3::ZERO, Vec3::Y);
140    }
141}
142
143/// Alters the hue of the solid color a bit every frame.
144fn shift_hue(
145    mut app_status: ResMut<AppStatus>,
146    objects_with_materials: Query<&MeshMaterial3d<StandardMaterial>>,
147    mut standard_materials: ResMut<Assets<StandardMaterial>>,
148) {
149    if app_status.tint_type != TintType::Solid {
150        return;
151    }
152
153    app_status.hue += HUE_SHIFT_SPEED;
154
155    for material_handle in objects_with_materials.iter() {
156        let Some(material) = standard_materials.get_mut(material_handle) else {
157            continue;
158        };
159        material.specular_tint = Color::hsva(app_status.hue, 1.0, 1.0, 1.0);
160    }
161}
162
163impl AppStatus {
164    /// Returns appropriate help text that reflects the current app status.
165    fn create_text(&self) -> Text {
166        let tint_map_help_text = match self.tint_type {
167            TintType::Solid => SWITCH_TO_MAP_HELP_TEXT,
168            TintType::Map => SWITCH_TO_SOLID_TINT_HELP_TEXT,
169        };
170
171        Text::new(tint_map_help_text)
172    }
173}
174
175/// Changes the specular tint to a solid color or map when the user presses
176/// Space.
177fn toggle_specular_map(
178    keyboard: Res<ButtonInput<KeyCode>>,
179    mut app_status: ResMut<AppStatus>,
180    app_assets: Res<AppAssets>,
181    objects_with_materials: Query<&MeshMaterial3d<StandardMaterial>>,
182    mut standard_materials: ResMut<Assets<StandardMaterial>>,
183) {
184    if !keyboard.just_pressed(KeyCode::Space) {
185        return;
186    }
187
188    // Swap tint type.
189    app_status.tint_type = match app_status.tint_type {
190        TintType::Solid => TintType::Map,
191        TintType::Map => TintType::Solid,
192    };
193
194    for material_handle in objects_with_materials.iter() {
195        let Some(material) = standard_materials.get_mut(material_handle) else {
196            continue;
197        };
198
199        // Adjust the tint type.
200        match app_status.tint_type {
201            TintType::Solid => {
202                material.reflectance = 1.0;
203                material.specular_tint_texture = None;
204            }
205            TintType::Map => {
206                // Set reflectance to 2.0 to spread out the map's reflectance
207                // range from the default [0.0, 0.5] to [0.0, 1.0].
208                material.reflectance = 2.0;
209                // As the tint map is multiplied by the tint color, we set the
210                // latter to white so that only the map has an effect.
211                material.specular_tint = WHITE.into();
212                material.specular_tint_texture = Some(app_assets.noise_texture.clone());
213            }
214        };
215    }
216}
217
218/// Updates the help text at the bottom of the screen to reflect the current app
219/// status.
220fn update_text(mut text_query: Query<&mut Text>, app_status: Res<AppStatus>) {
221    for mut text in text_query.iter_mut() {
222        *text = app_status.create_text();
223    }
224}