use bevy::{ asset::LoadState, core_pipeline::Skybox, prelude::*, render::{ render_resource::{TextureViewDescriptor, TextureViewDimension}, renderer::RenderDevice, texture::CompressedImageFormats, }, }; fn main() { App::new() .add_systems(Startup, setup) .add_systems(Update, ( asset_loaded.after(load_cubemap_asset), handle_input )) .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest())) .add_plugins(CameraControllerPlugin) .run(); } #[derive(Resource)] struct Cubemap { is_loaded: bool, index: usize, image_handle: Handle, } const CUBEMAPS: &[(&str, CompressedImageFormats)] = &[ ( "textures/stars_cubemap.png", CompressedImageFormats::NONE, ), ]; fn setup(mut commands: Commands, asset_server: Res) { let skybox_handle = asset_server.load(CUBEMAPS[0].0); // camera commands.spawn(( Camera3dBundle { transform: Transform::from_xyz(0.0, 0.0, 8.0).looking_at(Vec3::ZERO, Vec3::Y), ..default() }, CameraController::default(), Skybox { image: skybox_handle.clone(), brightness: 150.0, }, )); commands.insert_resource(Cubemap { is_loaded: false, index: 0, image_handle: skybox_handle, }); } fn load_cubemap_asset( mut cubemap: ResMut, asset_server: Res, render_device: Res, ) { let supported_compressed_formats = CompressedImageFormats::from_features(render_device.features()); let mut new_index = cubemap.index; for _ in 0..CUBEMAPS.len() { new_index = (new_index + 1) % CUBEMAPS.len(); if supported_compressed_formats.contains(CUBEMAPS[new_index].1) { break; } info!("Skipping unsupported format: {:?}", CUBEMAPS[new_index]); } // Skip swapping to the same texture. Useful for when ktx2, zstd, or compressed texture support // is missing if new_index == cubemap.index { return; } cubemap.index = new_index; cubemap.image_handle = asset_server.load(CUBEMAPS[cubemap.index].0); cubemap.is_loaded = false; } fn asset_loaded( asset_server: Res, mut images: ResMut>, mut cubemap: ResMut, mut skyboxes: Query<&mut Skybox>, ) { if !cubemap.is_loaded && asset_server.load_state(&cubemap.image_handle) == LoadState::Loaded { info!("Swapping to {}...", CUBEMAPS[cubemap.index].0); let image = images.get_mut(&cubemap.image_handle).unwrap(); // NOTE: PNGs do not have any metadata that could indicate they contain a cubemap texture, // so they appear as one texture. The following code reconfigures the texture as necessary. if image.texture_descriptor.array_layer_count() == 1 { image.reinterpret_stacked_2d_as_array(image.height() / image.width()); image.texture_view_descriptor = Some(TextureViewDescriptor { dimension: Some(TextureViewDimension::Cube), ..default() }); } for mut skybox in &mut skyboxes { skybox.image = cubemap.image_handle.clone(); } cubemap.is_loaded = true; } } fn handle_input( keyboard_input: Res>, mut app_exit_events: ResMut> ) { if keyboard_input.pressed(KeyCode::KeyQ) { app_exit_events.send(bevy::app::AppExit); } } // ------------------------------------------------------------------------------ // --------- copy&pasted from bevy's helpers/camera_controller.rs --------------- // ------------------------------------------------------------------------------ use bevy::input::mouse::MouseMotion; use bevy::window::CursorGrabMode; use std::{f32::consts::*, fmt}; pub struct CameraControllerPlugin; impl Plugin for CameraControllerPlugin { fn build(&self, app: &mut App) { app.add_systems(Update, run_camera_controller); } } /// Based on Valorant's default sensitivity, not entirely sure why it is exactly 1.0 / 180.0, /// but I'm guessing it is a misunderstanding between degrees/radians and then sticking with /// it because it felt nice. pub const RADIANS_PER_DOT: f32 = 1.0 / 180.0 * 0.5; #[derive(Component)] pub struct CameraController { pub enabled: bool, pub initialized: bool, pub sensitivity: f32, pub key_forward: KeyCode, pub key_back: KeyCode, pub key_left: KeyCode, pub key_right: KeyCode, pub key_up: KeyCode, pub key_down: KeyCode, pub key_run: KeyCode, pub mouse_key_cursor_grab: MouseButton, pub keyboard_key_toggle_cursor_grab: KeyCode, pub walk_speed: f32, pub run_speed: f32, pub friction: f32, pub pitch: f32, pub yaw: f32, pub velocity: Vec3, } impl Default for CameraController { fn default() -> Self { Self { enabled: true, initialized: false, sensitivity: 1.0, key_forward: KeyCode::KeyW, key_back: KeyCode::KeyS, key_left: KeyCode::KeyA, key_right: KeyCode::KeyD, key_up: KeyCode::KeyE, key_down: KeyCode::KeyQ, key_run: KeyCode::ShiftLeft, mouse_key_cursor_grab: MouseButton::Left, keyboard_key_toggle_cursor_grab: KeyCode::KeyM, walk_speed: 5.0, run_speed: 15.0, friction: 0.5, pitch: 0.0, yaw: 0.0, velocity: Vec3::ZERO, } } } impl fmt::Display for CameraController { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, " Freecam Controls: Mouse\t- Move camera orientation {:?}\t- Hold to grab cursor {:?}\t- Toggle cursor grab {:?} & {:?}\t- Fly forward & backwards {:?} & {:?}\t- Fly sideways left & right {:?} & {:?}\t- Fly up & down {:?}\t- Fly faster while held", self.mouse_key_cursor_grab, self.keyboard_key_toggle_cursor_grab, self.key_forward, self.key_back, self.key_left, self.key_right, self.key_up, self.key_down, self.key_run, ) } } #[allow(clippy::too_many_arguments)] fn run_camera_controller( time: Res