1use std::f32::consts::PI;
21
22use bevy::{
23 camera::Hdr,
24 color::palettes::css::{BLUE, GOLD, WHITE},
25 core_pipeline::tonemapping::Tonemapping::AcesFitted,
26 feathers::{theme::UiTheme, FeathersPlugins},
27 image::ImageLoaderSettings,
28 light::Skybox,
29 math::vec3,
30 prelude::*,
31 ui_widgets::{radio_self_update, ValueChange},
32};
33
34use radio::{feathers_option_buttons, main_ui_node_scene, RadioButtonOptionValue};
35
36#[path = "../helpers/radio.rs"]
37mod radio;
38
39#[path = "../helpers/theme.rs"]
40mod theme;
41
42const SPHERE_SCALE: f32 = 0.9;
44
45const SPHERE_ROTATION_SPEED: f32 = 0.8;
47
48#[derive(Clone, Copy, PartialEq, Resource, Default)]
50enum LightMode {
51 #[default]
52 Point,
53 Directional,
54}
55
56#[derive(Component)]
58struct ExampleSphere;
59
60pub fn main() {
62 App::new()
63 .init_resource::<LightMode>()
64 .add_plugins(DefaultPlugins)
65 .add_plugins(FeathersPlugins)
66 .insert_resource(UiTheme(theme::basic_example_theme(Color::WHITE)))
67 .add_systems(Startup, setup)
68 .add_systems(Update, animate_light)
69 .add_systems(Update, animate_spheres)
70 .add_observer(handle_selection_change)
71 .add_observer(radio_self_update)
72 .run();
73}
74
75fn setup(
77 mut commands: Commands,
78 mut meshes: ResMut<Assets<Mesh>>,
79 mut materials: ResMut<Assets<StandardMaterial>>,
80 asset_server: Res<AssetServer>,
81 light_mode: Res<LightMode>,
82) {
83 let sphere = create_sphere_mesh(&mut meshes);
84 spawn_car_paint_sphere(&mut commands, &mut materials, &asset_server, &sphere);
85 spawn_coated_glass_bubble_sphere(&mut commands, &mut materials, &sphere);
86 spawn_golf_ball(&mut commands, &asset_server);
87 spawn_scratched_gold_ball(&mut commands, &mut materials, &asset_server, &sphere);
88
89 spawn_light(&mut commands);
90 spawn_camera(&mut commands, &asset_server);
91 spawn_buttons(&mut commands, light_mode);
92}
93
94fn create_sphere_mesh(meshes: &mut Assets<Mesh>) -> Handle<Mesh> {
96 let mut sphere_mesh = Sphere::new(1.0).mesh().build();
100 sphere_mesh
101 .generate_tangents()
102 .expect("Failed to generate tangents");
103 meshes.add(sphere_mesh)
104}
105
106fn spawn_car_paint_sphere(
108 commands: &mut Commands,
109 materials: &mut Assets<StandardMaterial>,
110 asset_server: &AssetServer,
111 sphere: &Handle<Mesh>,
112) {
113 commands
114 .spawn((
115 Mesh3d(sphere.clone()),
116 MeshMaterial3d(
117 materials.add(StandardMaterial {
118 clearcoat: 1.0,
119 clearcoat_perceptual_roughness: 0.1,
120 normal_map_texture: Some(
121 asset_server
122 .load_builder()
123 .with_settings(|settings: &mut ImageLoaderSettings| {
124 settings.is_srgb = false;
125 })
126 .load("textures/BlueNoise-Normal.png"),
127 ),
128 metallic: 0.9,
129 perceptual_roughness: 0.5,
130 base_color: BLUE.into(),
131 ..default()
132 }),
133 ),
134 Transform::from_xyz(-1.0, 1.0, 0.0).with_scale(Vec3::splat(SPHERE_SCALE)),
135 ))
136 .insert(ExampleSphere);
137}
138
139fn spawn_coated_glass_bubble_sphere(
141 commands: &mut Commands,
142 materials: &mut Assets<StandardMaterial>,
143 sphere: &Handle<Mesh>,
144) {
145 commands
146 .spawn((
147 Mesh3d(sphere.clone()),
148 MeshMaterial3d(materials.add(StandardMaterial {
149 clearcoat: 1.0,
150 clearcoat_perceptual_roughness: 0.1,
151 metallic: 0.5,
152 perceptual_roughness: 0.1,
153 base_color: Color::srgba(0.9, 0.9, 0.9, 0.3),
154 alpha_mode: AlphaMode::Blend,
155 ..default()
156 })),
157 Transform::from_xyz(-1.0, -1.0, 0.0).with_scale(Vec3::splat(SPHERE_SCALE)),
158 ))
159 .insert(ExampleSphere);
160}
161
162fn spawn_golf_ball(commands: &mut Commands, asset_server: &AssetServer) {
168 commands.spawn((
169 WorldAssetRoot(
170 asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/GolfBall/GolfBall.glb")),
171 ),
172 Transform::from_xyz(1.0, 1.0, 0.0).with_scale(Vec3::splat(SPHERE_SCALE)),
173 ExampleSphere,
174 ));
175}
176
177fn spawn_scratched_gold_ball(
180 commands: &mut Commands,
181 materials: &mut Assets<StandardMaterial>,
182 asset_server: &AssetServer,
183 sphere: &Handle<Mesh>,
184) {
185 commands
186 .spawn((
187 Mesh3d(sphere.clone()),
188 MeshMaterial3d(
189 materials.add(StandardMaterial {
190 clearcoat: 1.0,
191 clearcoat_perceptual_roughness: 0.3,
192 clearcoat_normal_texture: Some(
193 asset_server
194 .load_builder()
195 .with_settings(|settings: &mut ImageLoaderSettings| {
196 settings.is_srgb = false;
197 })
198 .load("textures/ScratchedGold-Normal.png"),
199 ),
200 metallic: 0.9,
201 perceptual_roughness: 0.1,
202 base_color: GOLD.into(),
203 ..default()
204 }),
205 ),
206 Transform::from_xyz(1.0, -1.0, 0.0).with_scale(Vec3::splat(SPHERE_SCALE)),
207 ))
208 .insert(ExampleSphere);
209}
210
211fn spawn_light(commands: &mut Commands) {
213 commands.spawn(create_point_light());
214}
215
216fn spawn_camera(commands: &mut Commands, asset_server: &AssetServer) {
218 commands
219 .spawn((
220 Camera3d::default(),
221 Hdr,
222 Projection::Perspective(PerspectiveProjection {
223 fov: 27.0 / 180.0 * PI,
224 ..default()
225 }),
226 Transform::from_xyz(0.0, 0.0, 10.0),
227 AcesFitted,
228 ))
229 .insert(Skybox {
230 brightness: 5000.0,
231 image: Some(asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2")),
232 ..default()
233 })
234 .insert(EnvironmentMapLight {
235 diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
236 specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
237 intensity: 2000.0,
238 ..default()
239 });
240}
241
242fn animate_light(
244 mut lights: Query<&mut Transform, Or<(With<PointLight>, With<DirectionalLight>)>>,
245 time: Res<Time>,
246) {
247 let now = time.elapsed_secs();
248 for mut transform in lights.iter_mut() {
249 transform.translation = vec3(
250 ops::sin(now * 1.4),
251 ops::cos(now * 1.0),
252 ops::cos(now * 0.6),
253 ) * vec3(3.0, 4.0, 3.0);
254 transform.look_at(Vec3::ZERO, Vec3::Y);
255 }
256}
257
258fn animate_spheres(mut spheres: Query<&mut Transform, With<ExampleSphere>>, time: Res<Time>) {
260 let now = time.elapsed_secs();
261 for mut transform in spheres.iter_mut() {
262 transform.rotation = Quat::from_rotation_y(SPHERE_ROTATION_SPEED * now);
263 }
264}
265
266fn spawn_buttons(commands: &mut Commands, light_mode: Res<LightMode>) {
269 commands.spawn_scene(bsn! {
270 @main_ui_node_scene()
271 Children [
272 @feathers_option_buttons(
273 "Toggle light type",
274 &[
275 (LightMode::Directional, "Directional"),
276 (LightMode::Point, "Point"),
277 ],
278 if *light_mode == LightMode::Directional { 0 } else { 1 },
279 )
280 ]
281 });
282}
283
284fn handle_selection_change(
286 event: On<ValueChange<Entity>>,
287 mut commands: Commands,
288 mut light_query: Query<Entity, Or<(With<PointLight>, With<DirectionalLight>)>>,
289 new_value_query: Query<&RadioButtonOptionValue<LightMode>>,
290 mut light_mode: ResMut<LightMode>,
291) {
292 let Ok(RadioButtonOptionValue(selection)) = new_value_query.get(event.value) else {
293 return;
294 };
295
296 *light_mode = *selection;
297 for light in light_query.iter_mut() {
298 match *light_mode {
299 LightMode::Directional => {
300 commands
301 .entity(light)
302 .remove::<PointLight>()
303 .insert(create_directional_light());
304 }
305 LightMode::Point => {
306 commands
307 .entity(light)
308 .remove::<DirectionalLight>()
309 .insert(create_point_light());
310 }
311 }
312 }
313}
314
315fn create_point_light() -> PointLight {
317 PointLight {
318 color: WHITE.into(),
319 intensity: 100000.0,
320 ..default()
321 }
322}
323
324fn create_directional_light() -> DirectionalLight {
326 DirectionalLight {
327 color: WHITE.into(),
328 illuminance: 1000.0,
329 ..default()
330 }
331}