use std::f32::consts::{FRAC_PI_2, FRAC_PI_3, FRAC_PI_4, PI};
use std::fmt::{self, Formatter};
use bevy::{
camera::primitives::CubemapLayout,
color::palettes::css::{SILVER, YELLOW},
input::mouse::AccumulatedMouseMotion,
light::{DirectionalLightTexture, NotShadowCaster, PointLightTexture, SpotLightTexture},
pbr::decal,
prelude::*,
render::renderer::{RenderAdapter, RenderDevice},
window::{CursorIcon, SystemCursorIcon},
};
use light_consts::lux::{AMBIENT_DAYLIGHT, CLEAR_SUNRISE};
use ops::{acos, cos, sin};
use widgets::{
WidgetClickEvent, WidgetClickSender, BUTTON_BORDER, BUTTON_BORDER_COLOR,
BUTTON_BORDER_RADIUS_SIZE, BUTTON_PADDING,
};
#[path = "../helpers/widgets.rs"]
mod widgets;
const CUBE_ROTATION_SPEED: f32 = 0.02;
const MOVE_SPEED: f32 = 0.008;
const SCALE_SPEED: f32 = 0.05;
const ROLL_SPEED: f32 = 0.01;
#[derive(Resource, Default)]
struct AppStatus {
selection: Selection,
drag_mode: DragMode,
}
#[derive(Clone, Copy, Component, Default, PartialEq)]
enum Selection {
#[default]
Camera,
SpotLight,
PointLight,
DirectionalLight,
}
impl fmt::Display for Selection {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
Selection::Camera => f.write_str("camera"),
Selection::SpotLight => f.write_str("spotlight"),
Selection::PointLight => f.write_str("point light"),
Selection::DirectionalLight => f.write_str("directional light"),
}
}
}
#[derive(Clone, Copy, Component, Default, PartialEq, Debug)]
enum DragMode {
#[default]
Move,
Scale,
Roll,
}
impl fmt::Display for DragMode {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
DragMode::Move => f.write_str("move"),
DragMode::Scale => f.write_str("scale"),
DragMode::Roll => f.write_str("roll"),
}
}
}
#[derive(Clone, Copy, Component)]
struct HelpText;
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Bevy Light Textures Example".into(),
..default()
}),
..default()
}))
.init_resource::<AppStatus>()
.add_event::<WidgetClickEvent<Selection>>()
.add_event::<WidgetClickEvent<Visibility>>()
.add_systems(Startup, setup)
.add_systems(Update, draw_gizmos)
.add_systems(Update, rotate_cube)
.add_systems(Update, hide_shadows)
.add_systems(Update, widgets::handle_ui_interactions::<Selection>)
.add_systems(Update, widgets::handle_ui_interactions::<Visibility>)
.add_systems(
Update,
(handle_selection_change, update_radio_buttons)
.after(widgets::handle_ui_interactions::<Selection>)
.after(widgets::handle_ui_interactions::<Visibility>),
)
.add_systems(Update, toggle_visibility)
.add_systems(Update, update_directional_light)
.add_systems(Update, process_move_input)
.add_systems(Update, process_scale_input)
.add_systems(Update, process_roll_input)
.add_systems(Update, switch_drag_mode)
.add_systems(Update, update_help_text)
.add_systems(Update, update_button_visibility)
.run();
}
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
app_status: Res<AppStatus>,
render_device: Res<RenderDevice>,
render_adapter: Res<RenderAdapter>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
if !decal::clustered::clustered_decals_are_usable(&render_device, &render_adapter) {
error!("Light textures aren't usable on this platform.");
commands.write_event(AppExit::error());
}
spawn_cubes(&mut commands, &mut meshes, &mut materials);
spawn_camera(&mut commands);
spawn_light(&mut commands, &asset_server);
spawn_buttons(&mut commands);
spawn_help_text(&mut commands, &app_status);
spawn_light_textures(&mut commands, &asset_server, &mut meshes, &mut materials);
}
#[derive(Component)]
struct Rotate;
fn spawn_cubes(
commands: &mut Commands,
meshes: &mut Assets<Mesh>,
materials: &mut Assets<StandardMaterial>,
) {
let mut transform = Transform::IDENTITY;
transform.rotate_y(FRAC_PI_3);
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(3.0, 3.0, 3.0))),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: SILVER.into(),
..default()
})),
transform,
Rotate,
));
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(-13.0, -13.0, -13.0))),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: SILVER.into(),
..default()
})),
transform,
));
}
fn spawn_light(commands: &mut Commands, asset_server: &AssetServer) {
commands.spawn((
Visibility::Hidden,
Transform::from_xyz(8.0, 8.0, 4.0).looking_at(Vec3::ZERO, Vec3::Y),
Selection::DirectionalLight,
children![(
DirectionalLight {
illuminance: AMBIENT_DAYLIGHT,
..default()
},
DirectionalLightTexture {
image: asset_server.load("lightmaps/caustic_directional_texture.png"),
tiled: true,
},
Visibility::Visible,
)],
));
}
fn spawn_camera(commands: &mut Commands) {
commands
.spawn(Camera3d::default())
.insert(Transform::from_xyz(0.0, 2.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y))
.insert(Selection::Camera);
}
fn spawn_light_textures(
commands: &mut Commands,
asset_server: &AssetServer,
meshes: &mut Assets<Mesh>,
materials: &mut Assets<StandardMaterial>,
) {
commands.spawn((
SpotLight {
color: Color::srgb(1.0, 1.0, 0.8),
intensity: 10e6,
outer_angle: 0.25,
inner_angle: 0.25,
shadows_enabled: true,
..default()
},
Transform::from_translation(Vec3::new(6.0, 1.0, 2.0)).looking_at(Vec3::ZERO, Vec3::Y),
SpotLightTexture {
image: asset_server.load("lightmaps/torch_spotlight_texture.png"),
},
Visibility::Inherited,
Selection::SpotLight,
));
commands.spawn((
Visibility::Hidden,
Transform::from_translation(Vec3::new(0.0, 1.8, 0.01)).with_scale(Vec3::splat(0.1)),
Selection::PointLight,
children![
SceneRoot(
asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/Faces/faces.glb")),
),
(
Mesh3d(meshes.add(Sphere::new(1.0))),
MeshMaterial3d(materials.add(StandardMaterial {
emissive: Color::srgb(0.0, 0.0, 300.0).to_linear(),
..default()
})),
),
(
PointLight {
color: Color::srgb(0.0, 0.0, 1.0),
intensity: 1e6,
shadows_enabled: true,
..default()
},
PointLightTexture {
image: asset_server.load("lightmaps/faces_pointlight_texture_blurred.png"),
cubemap_layout: CubemapLayout::CrossVertical,
},
)
],
));
}
fn spawn_buttons(commands: &mut Commands) {
commands.spawn((
widgets::main_ui_node(),
children![widgets::option_buttons(
"Drag to Move",
&[
(Selection::Camera, "Camera"),
(Selection::SpotLight, "Spotlight"),
(Selection::PointLight, "Point Light"),
(Selection::DirectionalLight, "Directional Light"),
],
)],
));
commands.spawn((
Node {
flex_direction: FlexDirection::Row,
position_type: PositionType::Absolute,
right: px(10),
bottom: px(10),
column_gap: px(6),
..default()
},
children![
widgets::option_buttons(
"",
&[
(Visibility::Inherited, "Show"),
(Visibility::Hidden, "Hide"),
],
),
(drag_button("Scale"), DragMode::Scale),
(drag_button("Roll"), DragMode::Roll),
],
));
}
fn drag_button(label: &str) -> impl Bundle {
(
Node {
border: BUTTON_BORDER,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
padding: BUTTON_PADDING,
..default()
},
Button,
BackgroundColor(Color::BLACK),
BorderRadius::all(BUTTON_BORDER_RADIUS_SIZE),
BUTTON_BORDER_COLOR,
children![widgets::ui_text(label, Color::WHITE),],
)
}
fn spawn_help_text(commands: &mut Commands, app_status: &AppStatus) {
commands.spawn((
Text::new(create_help_string(app_status)),
Node {
position_type: PositionType::Absolute,
top: px(12),
left: px(12),
..default()
},
HelpText,
));
}
fn draw_gizmos(mut gizmos: Gizmos, spotlight: Query<(&GlobalTransform, &SpotLight, &Visibility)>) {
if let Ok((global_transform, spotlight, visibility)) = spotlight.single()
&& visibility != Visibility::Hidden
{
gizmos.primitive_3d(
&Cone::new(7.0 * spotlight.outer_angle, 7.0),
Isometry3d {
rotation: global_transform.rotation() * Quat::from_rotation_x(FRAC_PI_2),
translation: global_transform.translation_vec3a() * 0.5,
},
YELLOW,
);
}
}
fn rotate_cube(mut meshes: Query<&mut Transform, With<Rotate>>) {
for mut transform in &mut meshes {
transform.rotate_y(CUBE_ROTATION_SPEED);
}
}
fn hide_shadows(
mut commands: Commands,
meshes: Query<Entity, (With<Mesh3d>, Without<NotShadowCaster>, Without<Rotate>)>,
) {
for ent in &meshes {
commands.entity(ent).insert(NotShadowCaster);
}
}
fn update_radio_buttons(
mut widgets: Query<(
Entity,
Option<&mut BackgroundColor>,
Has<Text>,
&WidgetClickSender<Selection>,
)>,
app_status: Res<AppStatus>,
mut writer: TextUiWriter,
visible: Query<(&Visibility, &Selection)>,
mut visibility_widgets: Query<
(
Entity,
Option<&mut BackgroundColor>,
Has<Text>,
&WidgetClickSender<Visibility>,
),
Without<WidgetClickSender<Selection>>,
>,
) {
for (entity, maybe_bg_color, has_text, sender) in &mut widgets {
let selected = app_status.selection == **sender;
if let Some(mut bg_color) = maybe_bg_color {
widgets::update_ui_radio_button(&mut bg_color, selected);
}
if has_text {
widgets::update_ui_radio_button_text(entity, &mut writer, selected);
}
}
let visibility = visible
.iter()
.filter(|(_, selection)| **selection == app_status.selection)
.map(|(visibility, _)| *visibility)
.next()
.unwrap_or_default();
for (entity, maybe_bg_color, has_text, sender) in &mut visibility_widgets {
if let Some(mut bg_color) = maybe_bg_color {
widgets::update_ui_radio_button(&mut bg_color, **sender == visibility);
}
if has_text {
widgets::update_ui_radio_button_text(entity, &mut writer, **sender == visibility);
}
}
}
fn handle_selection_change(
mut events: EventReader<WidgetClickEvent<Selection>>,
mut app_status: ResMut<AppStatus>,
) {
for event in events.read() {
app_status.selection = **event;
}
}
fn toggle_visibility(
mut events: EventReader<WidgetClickEvent<Visibility>>,
app_status: Res<AppStatus>,
mut visibility: Query<(&mut Visibility, &Selection)>,
) {
if let Some(vis) = events.read().last() {
for (mut visibility, selection) in visibility.iter_mut() {
if selection == &app_status.selection {
*visibility = **vis;
}
}
}
}
fn process_move_input(
mut selections: Query<(&mut Transform, &Selection)>,
mouse_buttons: Res<ButtonInput<MouseButton>>,
mouse_motion: Res<AccumulatedMouseMotion>,
app_status: Res<AppStatus>,
) {
if !mouse_buttons.pressed(MouseButton::Left) || app_status.drag_mode != DragMode::Move {
return;
}
for (mut transform, selection) in &mut selections {
if app_status.selection != *selection {
continue;
}
if *selection == Selection::PointLight {
transform.translation +=
(mouse_motion.delta * Vec2::new(1.0, -1.0) * MOVE_SPEED).extend(0.0);
return;
}
let position = transform.translation;
let radius = position.length();
let mut theta = acos(position.y / radius);
let mut phi = position.z.signum() * acos(position.x * position.xz().length_recip());
let (phi_factor, theta_factor) = match *selection {
Selection::Camera => (1.0, -1.0),
_ => (-1.0, 1.0),
};
phi += phi_factor * mouse_motion.delta.x * MOVE_SPEED;
theta = f32::clamp(
theta + theta_factor * mouse_motion.delta.y * MOVE_SPEED,
0.001,
PI - 0.001,
);
transform.translation =
radius * vec3(sin(theta) * cos(phi), cos(theta), sin(theta) * sin(phi));
let roll = transform.rotation.to_euler(EulerRot::YXZ).2;
transform.look_at(Vec3::ZERO, Vec3::Y);
let (yaw, pitch, _) = transform.rotation.to_euler(EulerRot::YXZ);
transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll);
}
}
fn process_scale_input(
mut scale_selections: Query<(&mut Transform, &Selection)>,
mut spotlight_selections: Query<(&mut SpotLight, &Selection)>,
mouse_buttons: Res<ButtonInput<MouseButton>>,
mouse_motion: Res<AccumulatedMouseMotion>,
app_status: Res<AppStatus>,
) {
if !mouse_buttons.pressed(MouseButton::Left) || app_status.drag_mode != DragMode::Scale {
return;
}
for (mut transform, selection) in &mut scale_selections {
if app_status.selection == *selection {
transform.scale = (transform.scale * (1.0 + mouse_motion.delta.x * SCALE_SPEED))
.clamp(Vec3::splat(0.01), Vec3::splat(5.0));
}
}
for (mut spotlight, selection) in &mut spotlight_selections {
if app_status.selection == *selection {
spotlight.outer_angle = (spotlight.outer_angle
* (1.0 + mouse_motion.delta.x * SCALE_SPEED))
.clamp(0.01, FRAC_PI_4);
spotlight.inner_angle = spotlight.outer_angle;
}
}
}
fn process_roll_input(
mut selections: Query<(&mut Transform, &Selection)>,
mouse_buttons: Res<ButtonInput<MouseButton>>,
mouse_motion: Res<AccumulatedMouseMotion>,
app_status: Res<AppStatus>,
) {
if !mouse_buttons.pressed(MouseButton::Left) || app_status.drag_mode != DragMode::Roll {
return;
}
for (mut transform, selection) in &mut selections {
if app_status.selection != *selection {
continue;
}
let (yaw, pitch, mut roll) = transform.rotation.to_euler(EulerRot::YXZ);
roll += mouse_motion.delta.x * ROLL_SPEED;
transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll);
}
}
fn create_help_string(app_status: &AppStatus) -> String {
format!(
"Click and drag to {} {}",
app_status.drag_mode, app_status.selection
)
}
fn switch_drag_mode(
mut commands: Commands,
mut interactions: Query<(&Interaction, &DragMode)>,
mut windows: Query<Entity, With<Window>>,
mouse_buttons: Res<ButtonInput<MouseButton>>,
mut app_status: ResMut<AppStatus>,
) {
if mouse_buttons.pressed(MouseButton::Left) {
return;
}
for (interaction, drag_mode) in &mut interactions {
if *interaction != Interaction::Hovered {
continue;
}
app_status.drag_mode = *drag_mode;
for window in &mut windows {
commands
.entity(window)
.insert(CursorIcon::from(SystemCursorIcon::EwResize));
}
return;
}
app_status.drag_mode = DragMode::Move;
for window in &mut windows {
commands.entity(window).remove::<CursorIcon>();
}
}
fn update_help_text(mut help_text: Query<&mut Text, With<HelpText>>, app_status: Res<AppStatus>) {
for mut text in &mut help_text {
text.0 = create_help_string(&app_status);
}
}
fn update_button_visibility(
mut nodes: Query<&mut Visibility, Or<(With<DragMode>, With<WidgetClickSender<Visibility>>)>>,
app_status: Res<AppStatus>,
) {
for mut visibility in &mut nodes {
*visibility = match app_status.selection {
Selection::Camera => Visibility::Hidden,
_ => Visibility::Visible,
};
}
}
fn update_directional_light(
mut commands: Commands,
asset_server: Res<AssetServer>,
selections: Query<(&Selection, &Visibility)>,
mut light: Query<(
Entity,
&mut DirectionalLight,
Option<&DirectionalLightTexture>,
)>,
) {
let directional_visible = selections
.iter()
.filter(|(selection, _)| **selection == Selection::DirectionalLight)
.any(|(_, visibility)| visibility != Visibility::Hidden);
let any_texture_light_visible = selections
.iter()
.filter(|(selection, _)| {
**selection == Selection::PointLight || **selection == Selection::SpotLight
})
.any(|(_, visibility)| visibility != Visibility::Hidden);
let (entity, mut light, maybe_texture) = light
.single_mut()
.expect("there should be a single directional light");
if directional_visible {
light.illuminance = AMBIENT_DAYLIGHT;
if maybe_texture.is_none() {
commands.entity(entity).insert(DirectionalLightTexture {
image: asset_server.load("lightmaps/caustic_directional_texture.png"),
tiled: true,
});
}
} else if any_texture_light_visible {
light.illuminance = CLEAR_SUNRISE;
if maybe_texture.is_some() {
commands.entity(entity).remove::<DirectionalLightTexture>();
}
} else {
light.illuminance = AMBIENT_DAYLIGHT;
if maybe_texture.is_some() {
commands.entity(entity).remove::<DirectionalLightTexture>();
}
}
}