//! Demonstrates the normal map, metallic-roughness map, and emissive features //! of clustered decals. use std::{f32::consts::PI, time::Duration}; use bevy::{ asset::io::web::WebAssetPlugin, camera::Hdr, color::palettes::css::{CRIMSON, GOLD}, feathers::{dark_theme::create_dark_theme, theme::UiTheme, FeathersPlugins}, image::ImageLoaderSettings, light::ClusteredDecal, prelude::*, ui_widgets::{radio_self_update, ValueChange}, }; use chacha20::ChaCha8Rng; use rand::{RngExt, SeedableRng}; use crate::radio::{feathers_option_buttons, main_ui_node_scene, RadioButtonOptionValue}; #[path = "../helpers/radio.rs"] mod radio; /// The demonstration textures that we use. /// /// We cache these for efficiency. #[derive(Resource)] struct AppTextures { /// The base color that all our decals have (the Bevy logo). decal_base_color_texture: Handle, /// A normal map that all our decals have. /// /// This provides a nice raised embossed look. decal_normal_map_texture: Handle, /// The metallic-roughness map that all our decals have. /// /// Metallic is in the blue channel and roughness is in the green channel, /// like glTF requires. decal_metallic_roughness_map_texture: Handle, /// The emissive texture that can optionally be enabled. /// /// This causes the white bird to glow. decal_emissive_texture: Handle, } impl FromWorld for AppTextures { fn from_world(world: &mut World) -> Self { // Load all the decal textures. let asset_server = world.resource::(); AppTextures { decal_base_color_texture: asset_server.load("branding/bevy_bird_dark.png"), decal_normal_map_texture: asset_server .load_builder() .with_settings(|settings: &mut ImageLoaderSettings| settings.is_srgb = false) .load(get_web_asset_url("BevyLogo-Normal.png")), decal_metallic_roughness_map_texture: asset_server .load_builder() .with_settings(|settings: &mut ImageLoaderSettings| settings.is_srgb = false) .load(get_web_asset_url("BevyLogo-MetallicRoughness.png")), decal_emissive_texture: asset_server.load(get_web_asset_url("BevyLogo-Emissive.png")), } } } /// A component that we place on our decals to track them for animation /// purposes. #[derive(Component)] struct ExampleDecal { /// The width and height of the square decal in meters. size: f32, /// What state the decal is in (animating in, idling, or animating out). state: ExampleDecalState, } /// The animation state of a decal. /// /// When each [`Timer`] goes off, the decal advances to the next state. enum ExampleDecalState { /// The decal has just been spawned and is animating in. AnimatingIn(Timer), /// The decal has animated in and is waiting to animate out. Idling(Timer), /// The decal is animating out. /// /// When this timer expires, the decal is despawned. AnimatingOut(Timer), } /// All settings that the user can change. /// /// This app only has one: whether newly-spawned decals are emissive. #[derive(Clone, Copy, Component, PartialEq)] enum AppSetting { /// True if newly-spawned decals have an emissive channel (i.e. they glow), /// or false otherwise. EmissiveDecals(bool), } impl Default for AppSetting { fn default() -> Self { Self::EmissiveDecals(false) } } /// The current values of the settings that the user can change. /// /// This app only has one: whether newly-spawned decals are emissive. #[derive(Default, Resource)] struct AppStatus { /// True if newly-spawned decals have an emissive channel (i.e. they glow), /// or false otherwise. emissive_decals: bool, } /// Half of the width and height of the plane onto which the decals are /// projected. const PLANE_HALF_SIZE: f32 = 2.0; /// The minimum width and height that a decal may have. /// /// The actual size is determined randomly, using this value as a lower bound. const DECAL_MIN_SIZE: f32 = 0.5; /// The maximum width and height that a decal may have. /// /// The actual size is determined randomly, using this value as an upper bound. const DECAL_MAX_SIZE: f32 = 1.5; /// How long it takes the decal to grow to its full size when animating in. const DECAL_ANIMATE_IN_DURATION: Duration = Duration::from_millis(300); /// How long a decal stays in the idle state before starting to animate out. const DECAL_IDLE_DURATION: Duration = Duration::from_secs(10); /// How long it takes the decal to shrink down to nothing when animating out. const DECAL_ANIMATE_OUT_DURATION: Duration = Duration::from_millis(300); /// The demo entry point. fn main() { App::new() .add_plugins(( DefaultPlugins .set(WebAssetPlugin { silence_startup_warning: true, }) .set(WindowPlugin { primary_window: Some(Window { title: "Bevy Clustered Decal Maps Example".into(), ..default() }), ..default() }), FeathersPlugins, )) .init_resource::() .init_resource::() .insert_resource(UiTheme(create_dark_theme())) .add_systems(Startup, setup) .add_systems(Update, draw_gizmos) .add_systems(Update, spawn_decal) .add_systems(Update, animate_decals) .add_observer(handle_emission_type_change) .add_observer(radio_self_update) .insert_resource(SeededRng(ChaCha8Rng::seed_from_u64(19878367467712))) .run(); } #[derive(Resource)] struct SeededRng(ChaCha8Rng); /// Spawns all the objects in the scene. fn setup( mut commands: Commands, asset_server: Res, mut meshes: ResMut>, mut materials: ResMut>, ) { spawn_plane_mesh(&mut commands, &asset_server, &mut meshes, &mut materials); spawn_light(&mut commands); spawn_camera(&mut commands); spawn_buttons(&mut commands); } /// Spawns the plane onto which the decals are projected. fn spawn_plane_mesh( commands: &mut Commands, asset_server: &AssetServer, meshes: &mut Assets, materials: &mut Assets, ) { // Create a plane onto which we project decals. // // As the plane has a normal map, we must generate tangents for the // vertices. let plane_mesh = meshes.add( Plane3d { normal: Dir3::NEG_Z, half_size: Vec2::splat(PLANE_HALF_SIZE), } .mesh() .build() .with_duplicated_vertices() .with_computed_flat_normals() .with_generated_tangents() .unwrap(), ); // Give the plane some texture. // // Note that, as this is a normal map, we must disable sRGB when loading. let normal_map_texture = asset_server .load_builder() .with_settings(|settings: &mut ImageLoaderSettings| settings.is_srgb = false) .load("textures/ScratchedGold-Normal.png"); // Actually spawn the plane. commands.spawn(( Mesh3d(plane_mesh), MeshMaterial3d(materials.add(StandardMaterial { base_color: Color::from(CRIMSON), normal_map_texture: Some(normal_map_texture), ..StandardMaterial::default() })), Transform::IDENTITY, )); } /// Spawns a light to illuminate the scene. fn spawn_light(commands: &mut Commands) { commands.spawn(( PointLight { intensity: 10_000_000., range: 100.0, ..default() }, Transform::from_xyz(8.0, 16.0, -8.0), )); } /// Spawns a camera. fn spawn_camera(commands: &mut Commands) { commands.spawn(( Camera3d::default(), Transform::from_xyz(2.0, 0.0, -7.0).looking_at(Vec3::ZERO, Vec3::Y), Hdr, )); } /// Spawns all the buttons at the bottom of the screen. fn spawn_buttons(commands: &mut Commands) { commands.spawn_scene(bsn! { @main_ui_node_scene() Children [ @feathers_option_buttons( "Emissive Decals", &[ (AppSetting::EmissiveDecals(false), "Off"), (AppSetting::EmissiveDecals(true), "On"), ], 0, ) ] }); } /// Draws the outlines that show the bounds of the clustered decals. fn draw_gizmos(mut gizmos: Gizmos, decals: Query<&GlobalTransform, With>) { for global_transform in &decals { gizmos.primitive_3d( &Cuboid { // Since the clustered decal is a 1×1×1 cube in model space, its // half-size is half of the scaling part of its transform. half_size: global_transform.scale() * 0.5, }, Isometry3d { rotation: global_transform.rotation(), translation: global_transform.translation_vec3a(), }, GOLD, ); } } /// A system that spawns new decals at fixed intervals. fn spawn_decal( mut commands: Commands, app_status: Res, app_textures: Res, time: Res