use std::{f32::consts::PI, time::Duration};
use bevy::{
asset::io::web::WebAssetPlugin,
camera::Hdr,
color::palettes::css::{CRIMSON, GOLD},
image::ImageLoaderSettings,
light::ClusteredDecal,
prelude::*,
};
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
use crate::widgets::{RadioButton, RadioButtonText, WidgetClickEvent, WidgetClickSender};
#[path = "../helpers/widgets.rs"]
mod widgets;
#[derive(Resource)]
struct AppTextures {
decal_base_color_texture: Handle<Image>,
decal_normal_map_texture: Handle<Image>,
decal_metallic_roughness_map_texture: Handle<Image>,
decal_emissive_texture: Handle<Image>,
}
impl FromWorld for AppTextures {
fn from_world(world: &mut World) -> Self {
let asset_server = world.resource::<AssetServer>();
AppTextures {
decal_base_color_texture: asset_server.load("branding/bevy_bird_dark.png"),
decal_normal_map_texture: asset_server.load_with_settings(
get_web_asset_url("BevyLogo-Normal.png"),
|settings: &mut ImageLoaderSettings| settings.is_srgb = false,
),
decal_metallic_roughness_map_texture: asset_server.load_with_settings(
get_web_asset_url("BevyLogo-MetallicRoughness.png"),
|settings: &mut ImageLoaderSettings| settings.is_srgb = false,
),
decal_emissive_texture: asset_server.load(get_web_asset_url("BevyLogo-Emissive.png")),
}
}
}
#[derive(Component)]
struct ExampleDecal {
size: f32,
state: ExampleDecalState,
}
enum ExampleDecalState {
AnimatingIn(Timer),
Idling(Timer),
AnimatingOut(Timer),
}
#[derive(Clone, Copy, PartialEq)]
enum AppSetting {
EmissiveDecals(bool),
}
#[derive(Default, Resource)]
struct AppStatus {
emissive_decals: bool,
}
const PLANE_HALF_SIZE: f32 = 2.0;
const DECAL_MIN_SIZE: f32 = 0.5;
const DECAL_MAX_SIZE: f32 = 1.5;
const DECAL_ANIMATE_IN_DURATION: Duration = Duration::from_millis(300);
const DECAL_IDLE_DURATION: Duration = Duration::from_secs(10);
const DECAL_ANIMATE_OUT_DURATION: Duration = Duration::from_millis(300);
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()
}),
)
.add_message::<WidgetClickEvent<AppSetting>>()
.init_resource::<AppStatus>()
.init_resource::<AppTextures>()
.add_systems(Startup, setup)
.add_systems(Update, draw_gizmos)
.add_systems(Update, spawn_decal)
.add_systems(Update, animate_decals)
.add_systems(
Update,
(
widgets::handle_ui_interactions::<AppSetting>,
update_radio_buttons,
),
)
.add_systems(
Update,
handle_emission_type_change.after(widgets::handle_ui_interactions::<AppSetting>),
)
.insert_resource(SeededRng(ChaCha8Rng::seed_from_u64(19878367467712)))
.run();
}
#[derive(Resource)]
struct SeededRng(ChaCha8Rng);
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
spawn_plane_mesh(&mut commands, &asset_server, &mut meshes, &mut materials);
spawn_light(&mut commands);
spawn_camera(&mut commands);
spawn_buttons(&mut commands);
}
fn spawn_plane_mesh(
commands: &mut Commands,
asset_server: &AssetServer,
meshes: &mut Assets<Mesh>,
materials: &mut Assets<StandardMaterial>,
) {
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(),
);
let normal_map_texture = asset_server.load_with_settings(
"textures/ScratchedGold-Normal.png",
|settings: &mut ImageLoaderSettings| settings.is_srgb = false,
);
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,
));
}
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),
));
}
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,
));
}
fn spawn_buttons(commands: &mut Commands) {
commands.spawn((
widgets::main_ui_node(),
children![widgets::option_buttons(
"Emissive Decals",
&[
(AppSetting::EmissiveDecals(true), "On"),
(AppSetting::EmissiveDecals(false), "Off"),
],
),],
));
}
fn draw_gizmos(mut gizmos: Gizmos, decals: Query<&GlobalTransform, With<ClusteredDecal>>) {
for global_transform in &decals {
gizmos.primitive_3d(
&Cuboid {
half_size: global_transform.scale() * 0.5,
},
Isometry3d {
rotation: global_transform.rotation(),
translation: global_transform.translation_vec3a(),
},
GOLD,
);
}
}
fn spawn_decal(
mut commands: Commands,
app_status: Res<AppStatus>,
app_textures: Res<AppTextures>,
time: Res<Time>,
mut decal_spawn_timer: Local<Option<Timer>>,
mut seeded_rng: ResMut<SeededRng>,
) {
let decal_spawn_timer = decal_spawn_timer
.get_or_insert_with(|| Timer::new(Duration::from_millis(1000), TimerMode::Repeating));
decal_spawn_timer.tick(time.delta());
if !decal_spawn_timer.just_finished() {
return;
}
let decal_position = vec3(
seeded_rng.0.random_range(-PLANE_HALF_SIZE..PLANE_HALF_SIZE),
seeded_rng.0.random_range(-PLANE_HALF_SIZE..PLANE_HALF_SIZE),
0.0,
);
let decal_size = seeded_rng.0.random_range(DECAL_MIN_SIZE..DECAL_MAX_SIZE);
let theta = seeded_rng.0.random_range(0.0f32..PI);
commands.spawn((
ClusteredDecal {
base_color_texture: Some(app_textures.decal_base_color_texture.clone()),
normal_map_texture: Some(app_textures.decal_normal_map_texture.clone()),
metallic_roughness_texture: Some(
app_textures.decal_metallic_roughness_map_texture.clone(),
),
emissive_texture: if app_status.emissive_decals {
Some(app_textures.decal_emissive_texture.clone())
} else {
None
},
..ClusteredDecal::default()
},
Transform::from_translation(decal_position)
.with_scale(Vec3::ZERO)
.looking_to(Vec3::Z, Vec3::ZERO.with_xy(Vec2::from_angle(theta))),
ExampleDecal {
size: decal_size,
state: ExampleDecalState::AnimatingIn(Timer::new(
DECAL_ANIMATE_IN_DURATION,
TimerMode::Once,
)),
},
));
}
fn animate_decals(
mut commands: Commands,
mut decals_query: Query<(Entity, &mut ExampleDecal, &mut Transform)>,
time: Res<Time>,
) {
for (decal_entity, mut example_decal, mut decal_transform) in decals_query.iter_mut() {
match example_decal.state {
ExampleDecalState::AnimatingIn(ref mut timer) => {
timer.tick(time.delta());
if timer.just_finished() {
example_decal.state =
ExampleDecalState::Idling(Timer::new(DECAL_IDLE_DURATION, TimerMode::Once));
}
}
ExampleDecalState::Idling(ref mut timer) => {
timer.tick(time.delta());
if timer.just_finished() {
example_decal.state = ExampleDecalState::AnimatingOut(Timer::new(
DECAL_ANIMATE_OUT_DURATION,
TimerMode::Once,
));
}
}
ExampleDecalState::AnimatingOut(ref mut timer) => {
timer.tick(time.delta());
if timer.just_finished() {
commands.entity(decal_entity).despawn();
continue;
}
}
}
let new_decal_scale_factor = match example_decal.state {
ExampleDecalState::AnimatingIn(ref timer) => timer.fraction(),
ExampleDecalState::Idling(_) => 1.0,
ExampleDecalState::AnimatingOut(ref timer) => timer.fraction_remaining(),
};
decal_transform.scale =
Vec3::splat(example_decal.size * new_decal_scale_factor).with_z(1.0);
}
}
fn update_radio_buttons(
mut widgets: Query<
(
Entity,
Option<&mut BackgroundColor>,
Has<Text>,
&WidgetClickSender<AppSetting>,
),
Or<(With<RadioButton>, With<RadioButtonText>)>,
>,
app_status: Res<AppStatus>,
mut writer: TextUiWriter,
) {
for (entity, image, has_text, sender) in widgets.iter_mut() {
let selected = match **sender {
AppSetting::EmissiveDecals(emissive_decals) => {
emissive_decals == app_status.emissive_decals
}
};
if let Some(mut bg_color) = image {
widgets::update_ui_radio_button(&mut bg_color, selected);
}
if has_text {
widgets::update_ui_radio_button_text(entity, &mut writer, selected);
}
}
}
fn handle_emission_type_change(
mut app_status: ResMut<AppStatus>,
mut events: MessageReader<WidgetClickEvent<AppSetting>>,
) {
for event in events.read() {
let AppSetting::EmissiveDecals(on) = **event;
app_status.emissive_decals = on;
}
}
fn get_web_asset_url(name: &str) -> String {
format!(
"https://raw.githubusercontent.com/bevyengine/bevy_asset_files/refs/heads/main/\
clustered_decal_maps/{}",
name
)
}