use bevy::{
pbr::Lightmap,
picking::{backend::HitData, pointer::PointerInteraction},
prelude::*,
scene::SceneInstanceReady,
};
use crate::widgets::{RadioButton, RadioButtonText, WidgetClickEvent, WidgetClickSender};
#[path = "../helpers/widgets.rs"]
mod widgets;
const LIGHTMAP_EXPOSURE: f32 = 600.0;
const SPHERE_OFFSET: f32 = 0.2;
#[derive(Clone, Default, Resource)]
struct AppStatus {
lighting_mode: LightingMode,
}
#[derive(Clone, Copy, PartialEq, Default)]
enum LightingMode {
Baked,
MixedDirect,
#[default]
MixedIndirect,
RealTime,
}
#[derive(Clone, Copy, Default, Event)]
struct LightingModeChanged;
#[derive(Clone, Copy, Component, Debug)]
struct HelpText;
static LIGHTMAPS: [(&str, Rect); 5] = [
(
"Plane",
uv_rect_opengl(Vec2::splat(0.026), Vec2::splat(0.710)),
),
(
"SheenChair_fabric",
uv_rect_opengl(vec2(0.7864, 0.02377), vec2(0.1910, 0.1912)),
),
(
"SheenChair_label",
uv_rect_opengl(vec2(0.275, -0.016), vec2(0.858, 0.486)),
),
(
"SheenChair_metal",
uv_rect_opengl(vec2(0.998, 0.506), vec2(-0.029, -0.067)),
),
(
"SheenChair_wood",
uv_rect_opengl(vec2(0.787, 0.257), vec2(0.179, 0.177)),
),
];
static SPHERE_UV_RECT: Rect = uv_rect_opengl(vec2(0.788, 0.484), Vec2::splat(0.062));
const INITIAL_SPHERE_POSITION: Vec3 = vec3(0.0, 0.5233223, 0.0);
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Bevy Mixed Lighting Example".into(),
..default()
}),
..default()
}))
.add_plugins(MeshPickingPlugin)
.insert_resource(AmbientLight {
color: ClearColor::default().0,
brightness: 10000.0,
affects_lightmapped_meshes: true,
})
.init_resource::<AppStatus>()
.add_event::<WidgetClickEvent<LightingMode>>()
.add_event::<LightingModeChanged>()
.add_systems(Startup, setup)
.add_systems(Update, update_lightmaps)
.add_systems(Update, update_directional_light)
.add_systems(Update, make_sphere_nonpickable)
.add_systems(Update, update_radio_buttons)
.add_systems(Update, handle_lighting_mode_change)
.add_systems(Update, widgets::handle_ui_interactions::<LightingMode>)
.add_systems(Update, reset_sphere_position)
.add_systems(Update, move_sphere)
.add_systems(Update, adjust_help_text)
.run();
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>, app_status: Res<AppStatus>) {
spawn_camera(&mut commands);
spawn_scene(&mut commands, &asset_server);
spawn_buttons(&mut commands);
spawn_help_text(&mut commands, &app_status);
}
fn spawn_camera(commands: &mut Commands) {
commands
.spawn(Camera3d::default())
.insert(Transform::from_xyz(-0.7, 0.7, 1.0).looking_at(vec3(0.0, 0.3, 0.0), Vec3::Y));
}
fn spawn_scene(commands: &mut Commands, asset_server: &AssetServer) {
commands
.spawn(SceneRoot(
asset_server.load(
GltfAssetLabel::Scene(0)
.from_asset("models/MixedLightingExample/MixedLightingExample.gltf"),
),
))
.observe(
|_: Trigger<SceneInstanceReady>,
mut lighting_mode_change_event_writer: EventWriter<LightingModeChanged>| {
lighting_mode_change_event_writer.write(LightingModeChanged);
},
);
}
fn spawn_buttons(commands: &mut Commands) {
commands
.spawn(widgets::main_ui_node())
.with_children(|parent| {
widgets::spawn_option_buttons(
parent,
"Lighting",
&[
(LightingMode::Baked, "Baked"),
(LightingMode::MixedDirect, "Mixed (Direct)"),
(LightingMode::MixedIndirect, "Mixed (Indirect)"),
(LightingMode::RealTime, "Real-Time"),
],
);
});
}
fn spawn_help_text(commands: &mut Commands, app_status: &AppStatus) {
commands.spawn((
create_help_text(app_status),
Node {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
},
HelpText,
));
}
fn update_lightmaps(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut materials: ResMut<Assets<StandardMaterial>>,
meshes: Query<(Entity, &Name, &MeshMaterial3d<StandardMaterial>), With<Mesh3d>>,
mut lighting_mode_change_event_reader: EventReader<LightingModeChanged>,
app_status: Res<AppStatus>,
) {
if lighting_mode_change_event_reader.read().next().is_none() {
return;
}
let lightmap: Option<Handle<Image>> = match app_status.lighting_mode {
LightingMode::Baked => {
Some(asset_server.load("lightmaps/MixedLightingExample-Baked.zstd.ktx2"))
}
LightingMode::MixedDirect => {
Some(asset_server.load("lightmaps/MixedLightingExample-MixedDirect.zstd.ktx2"))
}
LightingMode::MixedIndirect => {
Some(asset_server.load("lightmaps/MixedLightingExample-MixedIndirect.zstd.ktx2"))
}
LightingMode::RealTime => None,
};
'outer: for (entity, name, material) in &meshes {
for (lightmap_name, uv_rect) in LIGHTMAPS {
if &**name != lightmap_name {
continue;
}
if let Some(ref mut material) = materials.get_mut(material) {
material.lightmap_exposure = LIGHTMAP_EXPOSURE;
}
match lightmap {
Some(ref lightmap) => {
commands.entity(entity).insert(Lightmap {
image: (*lightmap).clone(),
uv_rect,
bicubic_sampling: false,
});
}
None => {
commands.entity(entity).remove::<Lightmap>();
}
}
continue 'outer;
}
if &**name == "Sphere" {
if let Some(ref mut material) = materials.get_mut(material) {
material.lightmap_exposure = LIGHTMAP_EXPOSURE;
}
match (&lightmap, app_status.lighting_mode) {
(Some(lightmap), LightingMode::Baked) => {
commands.entity(entity).insert(Lightmap {
image: (*lightmap).clone(),
uv_rect: SPHERE_UV_RECT,
bicubic_sampling: false,
});
}
_ => {
commands.entity(entity).remove::<Lightmap>();
}
}
}
}
}
const fn uv_rect_opengl(gl_min: Vec2, size: Vec2) -> Rect {
let min = vec2(gl_min.x, 1.0 - gl_min.y - size.y);
Rect {
min,
max: vec2(min.x + size.x, min.y + size.y),
}
}
fn make_sphere_nonpickable(
mut commands: Commands,
mut query: Query<(Entity, &Name), (With<Mesh3d>, Without<Pickable>)>,
) {
for (sphere, name) in &mut query {
if &**name == "Sphere" {
commands.entity(sphere).insert(Pickable::IGNORE);
}
}
}
fn update_directional_light(
mut lights: Query<&mut DirectionalLight>,
mut lighting_mode_change_event_reader: EventReader<LightingModeChanged>,
app_status: Res<AppStatus>,
) {
if lighting_mode_change_event_reader.read().next().is_none() {
return;
}
let scenery_is_lit_in_real_time = matches!(
app_status.lighting_mode,
LightingMode::MixedIndirect | LightingMode::RealTime
);
for mut light in &mut lights {
light.affects_lightmapped_mesh_diffuse = scenery_is_lit_in_real_time;
light.shadows_enabled = scenery_is_lit_in_real_time;
}
}
fn update_radio_buttons(
mut widgets: Query<
(
Entity,
Option<&mut BackgroundColor>,
Has<Text>,
&WidgetClickSender<LightingMode>,
),
Or<(With<RadioButton>, With<RadioButtonText>)>,
>,
app_status: Res<AppStatus>,
mut writer: TextUiWriter,
) {
for (entity, image, has_text, sender) in &mut widgets {
let selected = **sender == app_status.lighting_mode;
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_lighting_mode_change(
mut widget_click_event_reader: EventReader<WidgetClickEvent<LightingMode>>,
mut lighting_mode_change_event_writer: EventWriter<LightingModeChanged>,
mut app_status: ResMut<AppStatus>,
) {
for event in widget_click_event_reader.read() {
app_status.lighting_mode = **event;
lighting_mode_change_event_writer.write(LightingModeChanged);
}
}
fn reset_sphere_position(
mut objects: Query<(&Name, &mut Transform)>,
mut lighting_mode_change_event_reader: EventReader<LightingModeChanged>,
app_status: Res<AppStatus>,
) {
if lighting_mode_change_event_reader.read().next().is_none()
|| app_status.lighting_mode != LightingMode::Baked
{
return;
}
for (name, mut transform) in &mut objects {
if &**name == "Sphere" {
transform.translation = INITIAL_SPHERE_POSITION;
break;
}
}
}
fn move_sphere(
mouse_button_input: Res<ButtonInput<MouseButton>>,
pointers: Query<&PointerInteraction>,
mut meshes: Query<(&Name, &ChildOf), With<Mesh3d>>,
mut transforms: Query<&mut Transform>,
app_status: Res<AppStatus>,
) {
if app_status.lighting_mode == LightingMode::Baked
|| !mouse_button_input.pressed(MouseButton::Left)
{
return;
}
let Some(child_of) = meshes
.iter_mut()
.filter_map(|(name, child_of)| {
if &**name == "Sphere" {
Some(child_of)
} else {
None
}
})
.next()
else {
return;
};
let Ok(mut transform) = transforms.get_mut(child_of.parent()) else {
return;
};
for interaction in pointers.iter() {
if let Some(&(
_,
HitData {
position: Some(position),
..
},
)) = interaction.get_nearest_hit()
{
transform.translation = position + vec3(0.0, SPHERE_OFFSET, 0.0);
}
}
}
fn adjust_help_text(
mut commands: Commands,
help_texts: Query<Entity, With<HelpText>>,
app_status: Res<AppStatus>,
mut lighting_mode_change_event_reader: EventReader<LightingModeChanged>,
) {
if lighting_mode_change_event_reader.read().next().is_none() {
return;
}
for help_text in &help_texts {
commands
.entity(help_text)
.insert(create_help_text(&app_status));
}
}
fn create_help_text(app_status: &AppStatus) -> Text {
match app_status.lighting_mode {
LightingMode::Baked => Text::new(
"Scenery: Static, baked direct light, baked indirect light
Sphere: Static, baked direct light, baked indirect light",
),
LightingMode::MixedDirect => Text::new(
"Scenery: Static, baked direct light, baked indirect light
Sphere: Dynamic, real-time direct light, no indirect light
Click in the scene to move the sphere",
),
LightingMode::MixedIndirect => Text::new(
"Scenery: Static, real-time direct light, baked indirect light
Sphere: Dynamic, real-time direct light, no indirect light
Click in the scene to move the sphere",
),
LightingMode::RealTime => Text::new(
"Scenery: Dynamic, real-time direct light, no indirect light
Sphere: Dynamic, real-time direct light, no indirect light
Click in the scene to move the sphere",
),
}
}