 1c765c9ae7
			
		
	
	
		1c765c9ae7
		
			
		
	
	
	
	
		
			
			This commit allows specular highlights to be tinted with a color and for the reflectance and color tint values to vary across a model via a pair of maps. The implementation follows the [`KHR_materials_specular`] glTF extension. In order to reduce the number of samplers and textures in the default `StandardMaterial` configuration, the maps are gated behind the `pbr_specular_textures` Cargo feature. Specular tinting is currently unsupported in the deferred renderer, because I didn't want to bloat the deferred G-buffers. A possible fix for this in the future would be to make the G-buffer layout more configurable, so that specular tints could be supported on an opt-in basis. As an alternative, Bevy could force meshes with specular tints to render in forward mode. Both of these solutions require some more design, so I consider them out of scope for now. Note that the map is a *specular* map, not a *reflectance* map. In Bevy and Filament terms, the reflectance values in the specular map range from [0.0, 0.5], rather than [0.0, 1.0]. This is an unfortunate [`KHR_materials_specular`] specification requirement that stems from the fact that glTF is specified in terms of a specular strength model, not the reflectance model that Filament and Bevy use. A workaround, which is noted in the `StandardMaterial` documentation, is to set the `reflectance` value to 2.0, which spreads the specular map range from [0.0, 1.0] as normal. The glTF loader has been updated to parse the [`KHR_materials_specular`] extension. Note that, unless the non-default `pbr_specular_textures` is supplied, the maps are ignored. The `specularFactor` value is applied as usual. Note that, as with the specular map, the glTF `specularFactor` is twice Bevy's `reflectance` value. This PR adds a new example, `specular_tint`, which demonstrates the specular tint and map features. Note that this example requires the [`KHR_materials_specular`] Cargo feature. [`KHR_materials_specular`]: https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_specular ## Changelog ### Added * Specular highlights can now be tinted with the `specular_tint` field in `StandardMaterial`. * Specular maps are now available in `StandardMaterial`, gated behind the `pbr_specular_textures` Cargo feature. * The `KHR_materials_specular` glTF extension is now supported, allowing for customization of specular reflectance and specular maps. Note that the latter are gated behind the `pbr_specular_textures` Cargo feature.
		
			
				
	
	
		
			228 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			228 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| //! 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<Image>,
 | |
| }
 | |
| 
 | |
| impl FromWorld for AppAssets {
 | |
|     fn from_world(world: &mut World) -> Self {
 | |
|         let asset_server = world.resource::<AssetServer>();
 | |
|         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::<AppAssets>()
 | |
|         .init_resource::<AppStatus>()
 | |
|         .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<AssetServer>,
 | |
|     app_status: Res<AppStatus>,
 | |
|     mut meshes: ResMut<Assets<Mesh>>,
 | |
|     mut standard_materials: ResMut<Assets<StandardMaterial>>,
 | |
| ) {
 | |
|     // 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:
 | |
|             //
 | |
|             // <https://google.github.io/filament/Filament.html#listing_fnormal>
 | |
|             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<Camera3d>>) {
 | |
|     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<AppStatus>,
 | |
|     objects_with_materials: Query<&MeshMaterial3d<StandardMaterial>>,
 | |
|     mut standard_materials: ResMut<Assets<StandardMaterial>>,
 | |
| ) {
 | |
|     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<ButtonInput<KeyCode>>,
 | |
|     mut app_status: ResMut<AppStatus>,
 | |
|     app_assets: Res<AppAssets>,
 | |
|     objects_with_materials: Query<&MeshMaterial3d<StandardMaterial>>,
 | |
|     mut standard_materials: ResMut<Assets<StandardMaterial>>,
 | |
| ) {
 | |
|     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<AppStatus>) {
 | |
|     for mut text in text_query.iter_mut() {
 | |
|         *text = app_status.create_text();
 | |
|     }
 | |
| }
 |