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 image::ImageLoaderSettings,
27 light::Skybox,
28 math::vec3,
29 prelude::*,
30};
31
32const SPHERE_SCALE: f32 = 0.9;
34
35const SPHERE_ROTATION_SPEED: f32 = 0.8;
37
38#[derive(Clone, Copy, PartialEq, Resource, Default)]
40enum LightMode {
41 #[default]
42 Point,
43 Directional,
44}
45
46#[derive(Component)]
48struct ExampleSphere;
49
50pub fn main() {
52 App::new()
53 .init_resource::<LightMode>()
54 .add_plugins(DefaultPlugins)
55 .add_systems(Startup, setup)
56 .add_systems(Update, animate_light)
57 .add_systems(Update, animate_spheres)
58 .add_systems(Update, (handle_input, update_help_text).chain())
59 .run();
60}
61
62fn setup(
64 mut commands: Commands,
65 mut meshes: ResMut<Assets<Mesh>>,
66 mut materials: ResMut<Assets<StandardMaterial>>,
67 asset_server: Res<AssetServer>,
68 light_mode: Res<LightMode>,
69) {
70 let sphere = create_sphere_mesh(&mut meshes);
71 spawn_car_paint_sphere(&mut commands, &mut materials, &asset_server, &sphere);
72 spawn_coated_glass_bubble_sphere(&mut commands, &mut materials, &sphere);
73 spawn_golf_ball(&mut commands, &asset_server);
74 spawn_scratched_gold_ball(&mut commands, &mut materials, &asset_server, &sphere);
75
76 spawn_light(&mut commands);
77 spawn_camera(&mut commands, &asset_server);
78 spawn_text(&mut commands, &light_mode);
79}
80
81fn create_sphere_mesh(meshes: &mut Assets<Mesh>) -> Handle<Mesh> {
83 let mut sphere_mesh = Sphere::new(1.0).mesh().build();
87 sphere_mesh
88 .generate_tangents()
89 .expect("Failed to generate tangents");
90 meshes.add(sphere_mesh)
91}
92
93fn spawn_car_paint_sphere(
95 commands: &mut Commands,
96 materials: &mut Assets<StandardMaterial>,
97 asset_server: &AssetServer,
98 sphere: &Handle<Mesh>,
99) {
100 commands
101 .spawn((
102 Mesh3d(sphere.clone()),
103 MeshMaterial3d(
104 materials.add(StandardMaterial {
105 clearcoat: 1.0,
106 clearcoat_perceptual_roughness: 0.1,
107 normal_map_texture: Some(
108 asset_server
109 .load_builder()
110 .with_settings(|settings: &mut ImageLoaderSettings| {
111 settings.is_srgb = false;
112 })
113 .load("textures/BlueNoise-Normal.png"),
114 ),
115 metallic: 0.9,
116 perceptual_roughness: 0.5,
117 base_color: BLUE.into(),
118 ..default()
119 }),
120 ),
121 Transform::from_xyz(-1.0, 1.0, 0.0).with_scale(Vec3::splat(SPHERE_SCALE)),
122 ))
123 .insert(ExampleSphere);
124}
125
126fn spawn_coated_glass_bubble_sphere(
128 commands: &mut Commands,
129 materials: &mut Assets<StandardMaterial>,
130 sphere: &Handle<Mesh>,
131) {
132 commands
133 .spawn((
134 Mesh3d(sphere.clone()),
135 MeshMaterial3d(materials.add(StandardMaterial {
136 clearcoat: 1.0,
137 clearcoat_perceptual_roughness: 0.1,
138 metallic: 0.5,
139 perceptual_roughness: 0.1,
140 base_color: Color::srgba(0.9, 0.9, 0.9, 0.3),
141 alpha_mode: AlphaMode::Blend,
142 ..default()
143 })),
144 Transform::from_xyz(-1.0, -1.0, 0.0).with_scale(Vec3::splat(SPHERE_SCALE)),
145 ))
146 .insert(ExampleSphere);
147}
148
149fn spawn_golf_ball(commands: &mut Commands, asset_server: &AssetServer) {
155 commands.spawn((
156 WorldAssetRoot(
157 asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/GolfBall/GolfBall.glb")),
158 ),
159 Transform::from_xyz(1.0, 1.0, 0.0).with_scale(Vec3::splat(SPHERE_SCALE)),
160 ExampleSphere,
161 ));
162}
163
164fn spawn_scratched_gold_ball(
167 commands: &mut Commands,
168 materials: &mut Assets<StandardMaterial>,
169 asset_server: &AssetServer,
170 sphere: &Handle<Mesh>,
171) {
172 commands
173 .spawn((
174 Mesh3d(sphere.clone()),
175 MeshMaterial3d(
176 materials.add(StandardMaterial {
177 clearcoat: 1.0,
178 clearcoat_perceptual_roughness: 0.3,
179 clearcoat_normal_texture: Some(
180 asset_server
181 .load_builder()
182 .with_settings(|settings: &mut ImageLoaderSettings| {
183 settings.is_srgb = false;
184 })
185 .load("textures/ScratchedGold-Normal.png"),
186 ),
187 metallic: 0.9,
188 perceptual_roughness: 0.1,
189 base_color: GOLD.into(),
190 ..default()
191 }),
192 ),
193 Transform::from_xyz(1.0, -1.0, 0.0).with_scale(Vec3::splat(SPHERE_SCALE)),
194 ))
195 .insert(ExampleSphere);
196}
197
198fn spawn_light(commands: &mut Commands) {
200 commands.spawn(create_point_light());
201}
202
203fn spawn_camera(commands: &mut Commands, asset_server: &AssetServer) {
205 commands
206 .spawn((
207 Camera3d::default(),
208 Hdr,
209 Projection::Perspective(PerspectiveProjection {
210 fov: 27.0 / 180.0 * PI,
211 ..default()
212 }),
213 Transform::from_xyz(0.0, 0.0, 10.0),
214 AcesFitted,
215 ))
216 .insert(Skybox {
217 brightness: 5000.0,
218 image: Some(asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2")),
219 ..default()
220 })
221 .insert(EnvironmentMapLight {
222 diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
223 specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
224 intensity: 2000.0,
225 ..default()
226 });
227}
228
229fn spawn_text(commands: &mut Commands, light_mode: &LightMode) {
231 commands.spawn((
232 light_mode.create_help_text(),
233 Node {
234 position_type: PositionType::Absolute,
235 bottom: px(12),
236 left: px(12),
237 ..default()
238 },
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 handle_input(
269 mut commands: Commands,
270 mut light_query: Query<Entity, Or<(With<PointLight>, With<DirectionalLight>)>>,
271 keyboard: Res<ButtonInput<KeyCode>>,
272 mut light_mode: ResMut<LightMode>,
273) {
274 if !keyboard.just_pressed(KeyCode::Space) {
275 return;
276 }
277
278 for light in light_query.iter_mut() {
279 match *light_mode {
280 LightMode::Point => {
281 *light_mode = LightMode::Directional;
282 commands
283 .entity(light)
284 .remove::<PointLight>()
285 .insert(create_directional_light());
286 }
287 LightMode::Directional => {
288 *light_mode = LightMode::Point;
289 commands
290 .entity(light)
291 .remove::<DirectionalLight>()
292 .insert(create_point_light());
293 }
294 }
295 }
296}
297
298fn update_help_text(mut text_query: Query<&mut Text>, light_mode: Res<LightMode>) {
300 for mut text in text_query.iter_mut() {
301 *text = light_mode.create_help_text();
302 }
303}
304
305fn create_point_light() -> PointLight {
307 PointLight {
308 color: WHITE.into(),
309 intensity: 100000.0,
310 ..default()
311 }
312}
313
314fn create_directional_light() -> DirectionalLight {
316 DirectionalLight {
317 color: WHITE.into(),
318 illuminance: 1000.0,
319 ..default()
320 }
321}
322
323impl LightMode {
324 fn create_help_text(&self) -> Text {
326 let help_text = match *self {
327 LightMode::Point => "Press Space to switch to a directional light",
328 LightMode::Directional => "Press Space to switch to a point light",
329 };
330
331 Text::new(help_text)
332 }
333}