//! Demonstrates specular tints and maps. use std::f32::consts::PI; use bevy::{color::palettes::css::WHITE, core_pipeline::Skybox, prelude::*}; /// The camera rotation speed in radians per frame. const ROTATION_SPEED: f32 = 0.005; /// The rate at which the specular tint hue changes in degrees per frame. const HUE_SHIFT_SPEED: f32 = 0.2; static SWITCH_TO_MAP_HELP_TEXT: &str = "Press Space to switch to a specular map"; static SWITCH_TO_SOLID_TINT_HELP_TEXT: &str = "Press Space to switch to a solid specular tint"; /// The current settings the user has chosen. #[derive(Resource, Default)] struct AppStatus { /// The type of tint (solid or texture map). tint_type: TintType, /// The hue of the solid tint in radians. hue: f32, } /// Assets needed by the demo. #[derive(Resource)] struct AppAssets { /// A color tileable 3D noise texture. noise_texture: Handle, } impl FromWorld for AppAssets { fn from_world(world: &mut World) -> Self { let asset_server = world.resource::(); Self { noise_texture: asset_server.load("textures/AlphaNoise.png"), } } } /// The type of specular tint that the user has selected. #[derive(Clone, Copy, PartialEq, Default)] enum TintType { /// A solid color. #[default] Solid, /// A Perlin noise texture. Map, } /// The entry point. fn main() { App::new() .add_plugins(DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { title: "Bevy Specular Tint Example".into(), ..default() }), ..default() })) .init_resource::() .init_resource::() .insert_resource(AmbientLight { color: Color::BLACK, brightness: 0.0, ..default() }) .add_systems(Startup, setup) .add_systems(Update, rotate_camera) .add_systems(Update, (toggle_specular_map, update_text).chain()) .add_systems(Update, shift_hue.after(toggle_specular_map)) .run(); } /// Creates the scene. fn setup( mut commands: Commands, asset_server: Res, app_status: Res, mut meshes: ResMut>, mut standard_materials: ResMut>, ) { // Spawns a camera. commands.spawn(( Transform::from_xyz(-2.0, 0.0, 3.5).looking_at(Vec3::ZERO, Vec3::Y), Camera { hdr: true, ..default() }, Camera3d::default(), Skybox { image: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), brightness: 3000.0, ..default() }, EnvironmentMapLight { diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), // We want relatively high intensity here in order for the specular // tint to show up well. intensity: 25000.0, ..default() }, )); // Spawn the sphere. commands.spawn(( Transform::from_rotation(Quat::from_rotation_x(PI * 0.5)), Mesh3d(meshes.add(Sphere::default().mesh().uv(32, 18))), MeshMaterial3d(standard_materials.add(StandardMaterial { // We want only reflected specular light here, so we set the base // color as black. base_color: Color::BLACK, reflectance: 1.0, specular_tint: Color::hsva(app_status.hue, 1.0, 1.0, 1.0), // The object must not be metallic, or else the reflectance is // ignored per the Filament spec: // // metallic: 0.0, perceptual_roughness: 0.0, ..default() })), )); // Spawn the help text. commands.spawn(( Node { position_type: PositionType::Absolute, bottom: Val::Px(12.0), left: Val::Px(12.0), ..default() }, app_status.create_text(), )); } /// Rotates the camera a bit every frame. fn rotate_camera(mut cameras: Query<&mut Transform, With>) { for mut camera_transform in cameras.iter_mut() { camera_transform.translation = Quat::from_rotation_y(ROTATION_SPEED) * camera_transform.translation; camera_transform.look_at(Vec3::ZERO, Vec3::Y); } } /// Alters the hue of the solid color a bit every frame. fn shift_hue( mut app_status: ResMut, objects_with_materials: Query<&MeshMaterial3d>, mut standard_materials: ResMut>, ) { if app_status.tint_type != TintType::Solid { return; } app_status.hue += HUE_SHIFT_SPEED; for material_handle in objects_with_materials.iter() { let Some(material) = standard_materials.get_mut(material_handle) else { continue; }; material.specular_tint = Color::hsva(app_status.hue, 1.0, 1.0, 1.0); } } impl AppStatus { /// Returns appropriate help text that reflects the current app status. fn create_text(&self) -> Text { let tint_map_help_text = match self.tint_type { TintType::Solid => SWITCH_TO_MAP_HELP_TEXT, TintType::Map => SWITCH_TO_SOLID_TINT_HELP_TEXT, }; Text::new(tint_map_help_text) } } /// Changes the specular tint to a solid color or map when the user presses /// Space. fn toggle_specular_map( keyboard: Res>, mut app_status: ResMut, app_assets: Res, objects_with_materials: Query<&MeshMaterial3d>, mut standard_materials: ResMut>, ) { if !keyboard.just_pressed(KeyCode::Space) { return; } // Swap tint type. app_status.tint_type = match app_status.tint_type { TintType::Solid => TintType::Map, TintType::Map => TintType::Solid, }; for material_handle in objects_with_materials.iter() { let Some(material) = standard_materials.get_mut(material_handle) else { continue; }; // Adjust the tint type. match app_status.tint_type { TintType::Solid => { material.reflectance = 1.0; material.specular_tint_texture = None; } TintType::Map => { // Set reflectance to 2.0 to spread out the map's reflectance // range from the default [0.0, 0.5] to [0.0, 1.0]. material.reflectance = 2.0; // As the tint map is multiplied by the tint color, we set the // latter to white so that only the map has an effect. material.specular_tint = WHITE.into(); material.specular_tint_texture = Some(app_assets.noise_texture.clone()); } }; } } /// Updates the help text at the bottom of the screen to reflect the current app /// status. fn update_text(mut text_query: Query<&mut Text>, app_status: Res) { for mut text in text_query.iter_mut() { *text = app_status.create_text(); } }