use bevy::{ core_pipeline::clear_color::ClearColorConfig, gltf::{Gltf, GltfNode}, input::{ keyboard::KeyboardInput, mouse::{MouseMotion, MouseWheel}, ButtonState, }, pbr::CascadeShadowConfigBuilder, prelude::*, render::{ camera::RenderTarget, render_resource::{ Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, }, view::VisibleEntities, }, window::PrimaryWindow, }; use bevy_rapier3d::{prelude::*, rapier::prelude::RigidBodyType}; use monologue_trees::{debug::*, text::*}; fn main() { App::new() .add_plugins(( DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { title: "GLTF Inspector".into(), resolution: (640., 480.).into(), ..default() }), ..default() }), DebugInfoPlugin, RapierPhysicsPlugin::::default(), RapierDebugRenderPlugin::default(), )) .add_event::() .add_event::() .add_systems(PreStartup, load_models) .add_systems(Startup, (spawn_base_scene, spawn_base_ui)) .add_systems( Update, ( spawn_models, spawn_ui, control_animation, rotate_model, zoom_model, scroll, select, manage_active, inspect_nodes, selection, ), ) .run(); } /// /// Stores GLTF handles for later use #[derive(Resource)] struct Models { handles: Vec>, } /// /// Marks GLTF model as inspectable #[derive(Component)] struct Inspect; #[derive(Component)] struct SelectionUI; #[derive(Component)] struct PreviewCamera; #[derive(Component)] struct ScrollingList; #[derive(Component)] struct Preview(Handle); #[derive(Component)] struct Container; #[derive(Component)] struct Active; /// /// Event for managing which entities are tagged "Active" #[derive(Event, Debug)] struct ManageActive(Option); /// /// Event for tracking which entities are selected #[derive(Event, Debug)] enum Selected { Hovered(Entity), Selected(Entity), } /// /// Load all GLTF models on startup fn load_models(mut commands: Commands, ass: Res) { let weak_handles = ass.load_folder("models").expect("Load gltfs"); let handles: Vec> = weak_handles .iter() .map(|weak| weak.clone().typed::()) .collect(); // info!("Scene Handles: {:#?}", handles); commands.insert_resource(Models { handles }); } /// /// Spawn base scene fn spawn_base_scene(mut commands: Commands) { commands.spawn(( Camera2dBundle { camera_2d: Camera2d { clear_color: ClearColorConfig::Custom(Color::BLACK), }, ..default() }, UiCameraConfig { ..default() }, SelectionUI, )); } fn spawn_base_ui(mut commands: Commands) { commands .spawn(( NodeBundle { style: Style { justify_content: JustifyContent::Center, width: Val::Percent(90.0), height: Val::Percent(90.0), overflow: Overflow::clip(), ..default() }, ..default() }, SelectionUI, )) .with_children(|parent| { parent.spawn(( NodeBundle { style: Style { flex_wrap: FlexWrap::Wrap, flex_direction: FlexDirection::Row, justify_content: JustifyContent::SpaceAround, width: Val::Auto, height: Val::Auto, ..default() }, ..default() }, ScrollingList, )); }); } /// /// Spawn a loaded scene for inspection /// TODO: Update/add/delete when files are updated fn spawn_models( mut commands: Commands, mut scene_evr: EventReader>, mut images: ResMut>, ) { if !scene_evr.is_empty() { info!("Spawning scenes"); } for ev in scene_evr.iter() { match ev { AssetEvent::Created { handle } => { info!("Creating scene {:#?}", handle); // Preview image let preview_image_handle = { info!("Creating preview"); let size = Extent3d { width: 256, height: 256, ..default() }; // Create render target image for the preview camera let mut image = Image { texture_descriptor: TextureDescriptor { label: None, size, dimension: TextureDimension::D2, format: TextureFormat::Bgra8UnormSrgb, mip_level_count: 1, sample_count: 1, usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::RENDER_ATTACHMENT, view_formats: &[], }, ..default() }; // Fill with zeroes image.resize(size); let image_handle = images.add(image); image_handle }; let local = { // Get a unique number for this resource from the handle let idx = handle.id().reflect_hash().unwrap(); // Set the origin of this scene to [idx, idx, idx]; let origin = Vec3::ONE * ((idx % 1000) as f32); // Transform pointing at origin Transform::from_translation(origin) }; // Spawn the actual scene commands .spawn(( SpatialBundle { transform: local, ..default() }, Inspect, Container, Preview(preview_image_handle.clone()), )) .with_children(|builder| { let camera_location = Transform::from_xyz(0.0, 0.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y); // Spawn preview camera builder.spawn(( Camera3dBundle { camera_3d: Camera3d { clear_color: ClearColorConfig::Custom(Color::WHITE), ..default() }, camera: Camera { order: -1, target: RenderTarget::Image(preview_image_handle.clone()), ..default() }, transform: camera_location, ..default() }, UiCameraConfig { show_ui: false }, PreviewCamera, )); // Spawn window camera builder.spawn(( Camera3dBundle { camera_3d: Camera3d { clear_color: ClearColorConfig::Custom(Color::WHITE), ..default() }, camera: Camera { is_active: false, ..default() }, transform: camera_location, ..default() }, Inspect, Preview(preview_image_handle.clone()), )); builder.spawn(( DirectionalLightBundle { directional_light: DirectionalLight { shadows_enabled: true, ..default() }, cascade_shadow_config: CascadeShadowConfigBuilder { num_cascades: 1, maximum_distance: 1.6, ..default() } .into(), ..default() }, Inspect, )); builder.spawn(( SceneBundle { scene: handle.clone(), ..default() }, RigidBody::KinematicPositionBased, Collider::cylinder(1.0, 1.0), Inspect, )); }); } AssetEvent::Removed { .. } => { todo!("Remove deleted scene") } AssetEvent::Modified { .. } => { todo!("Update modified scene") } } } } fn spawn_ui( mut commands: Commands, query: Query>, previews: Query<&Preview, (Added, Without, Without)>, ) { // UI container if let Ok(scrolling_list_container) = query.get_single() { let mut entity_commands = commands.entity(scrolling_list_container); entity_commands.with_children(|parent| { for Preview(image_handle) in previews.iter() { // Preview Image info!("Spawning image preview"); parent .spawn(( ButtonBundle { ..default() }, SelectionUI, Preview(image_handle.clone()), )) .with_children(|parent| { parent.spawn(ImageBundle { style: Style { width: Val::Px(256.0), height: Val::Px(256.0), padding: UiRect::all(Val::Px(5.0)), ..default() }, image: UiImage { texture: image_handle.clone(), ..default() }, ..default() }); }); } }); } } fn inspect_nodes(gltfs: Res>, nodes: Res>, mut done: Local) { if !(*done) { for gltf in gltfs.iter() { // info!("Gltf {:#?}", gltf); *done = true; } } } fn control_animation( mut key_evr: EventReader, mut active_evr: EventReader, mut players: Query<&mut AnimationPlayer>, children: Query<&Children>, models: Res, gltfs: Res>, mut active: Local>, mut playing: Local, mut dirty: Local, ) { for event in active_evr.iter() { match event { ManageActive(None) => { // world state must be updated *dirty = true; // Stop playing *playing = false; // Add this to a list of acive entities (*active).clear(); } ManageActive(Some(entity)) => { // world state must be updated *dirty = true; // Start playing *playing = true; // Add this to a list of acive entities (*active).push(*entity); } } } for event in key_evr.iter() { match event { KeyboardInput { key_code: Some(KeyCode::Space), state: ButtonState::Pressed, .. } => { // World state needs to be updated to deisred state *dirty = true; // Toggle playing *playing = !(*playing); } _ => (), } } // If world needs to be updated if *dirty { if *playing { for entity in active.iter() { for child in children.iter_descendants(*entity) { if let Ok(mut player) = players.get_mut(child) { for gltf_handle in models.handles.iter() { if let Some(gltf) = gltfs.get(gltf_handle) { for animation_handle in gltf.animations.iter() { player.start(animation_handle.clone()).repeat(); player.resume(); } } else { info!("Failed to get GLTF handle"); } } } } } } else { for mut player in players.iter_mut() { player.pause(); } } // Done making updates *dirty = false; } } /// /// Rotate a model as part of inspection /// TODO: move light with model (want to see shadows) fn rotate_model( buttons: Res>, mut mouse_evr: EventReader, mut transforms: Query<&mut Transform, (With, With, Without)>, ) { if buttons.pressed(MouseButton::Left) { for MouseMotion { delta } in mouse_evr.iter() { for mut transform in transforms.iter_mut() { let rot_y = delta.x / 1000.0; let rot_x = delta.y / 1000.0; transform.rotate_y(rot_y); transform.rotate_x(rot_x); } } } } /// /// Zoom in and out of the model /// TODO: Only modify selected entities fn zoom_model( keys: Res>, mut wheel_evr: EventReader, mut transforms: Query<&mut Transform, (With, With, Without)>, ) { if keys.pressed(KeyCode::ShiftLeft) { for ev in wheel_evr.iter() { for mut transform in transforms.iter_mut() { let scale = (Vec3::ONE * ev.y) / 100.0; transform.scale += scale; } } } } fn scroll( mut scroll_evr: EventReader, mut query: Query<&mut Style, With>, active: Query>, ) { for ev in scroll_evr.iter() { // Only scroll if scene not selected if active.is_empty() { for mut s in query.iter_mut() { s.top = match s.top { Val::Px(current) => Val::Px(current + (ev.y * 5.0)), _ => Val::Px(0.0), }; } } } } /// /// Click a UI element to select /// /// This is a really ugly implementation. I'm not really happy with how we have to navigate the ECS /// parent/child hierarchy. There should be a more direct way to correlate the scene with the /// button. fn select( query: Query<(&Interaction, &Preview), (With, Changed)>, mut selection_ui: Query<&mut Visibility, (With, Without)>, mut ui_camera: Query<&mut Camera, (With, Without)>, mut scene_camera: Query<(Entity, &mut Camera, &Preview), (With, Without)>, mut key_evr: EventReader, mut selected: Local>, // Active camera index parent_search: Query<&Children>, parents: Query, Without)>, // TODO: Constrain mut events: EventWriter, ) { for (interaction, selected_preview) in query.iter() { if interaction == &Interaction::Pressed { // Hide UI let mut ui_vis = selection_ui.single_mut(); *ui_vis = Visibility::Hidden; // Disable UI camera let mut ui_cam = ui_camera.single_mut(); ui_cam.is_active = false; // Determine selected scene *selected = scene_camera .iter() .find(|(_, _, preview)| selected_preview.0 == preview.0) .map(|(entity, _, _)| entity); // Enable scene camera let (_, mut scene_cam, _) = scene_camera .get_mut(selected.expect("Selected scene should be set")) .expect("Failed to get Scene camera"); scene_cam.is_active = true; // Set relevant entities active let message = parents.iter().find(|&parent| { parent_search .iter_descendants(parent) .find(|&entity| Some(entity) == *selected) .is_some() }); events.send(ManageActive(message)); } } for ev in key_evr.iter() { match ev { KeyboardInput { state: ButtonState::Pressed, key_code: Some(KeyCode::Escape), .. } => { if let Some(s) = *selected { // Set all inactive events.send(ManageActive(None)); // Disable scene camera let (_, mut scene_cam, _) = scene_camera.get_mut(s).expect("Failed to get Scene camera"); scene_cam.is_active = false; // Enable UI camera let mut ui_cam = ui_camera.single_mut(); ui_cam.is_active = true; // Make UI visible let mut ui_vis = selection_ui.single_mut(); *ui_vis = Visibility::Inherited; } } _ => (), } } } fn manage_active( mut commands: Commands, mut events: EventReader, query: Query<&Children>, current: Query>, names: Query<&Name>, ) { for ManageActive(inner) in events.iter() { info!("Setting active: {:?}", inner); match inner { None => { for entity in current.iter() { if let Some(mut entity_commands) = commands.get_entity(entity) { entity_commands.remove::(); entity_commands.remove::(); } } } Some(entity) => { for child in query.iter_descendants(*entity) { if let Some(mut child_commands) = commands.get_entity(child) { // info!("Name: {:?}", names.get(child)); // child_commands.log_components(); child_commands.insert(Active); } } } } } } fn selection( mut commands: Commands, windows: Query<&Window, With>, mut mouse_events: EventReader, camera_q: Query<(&Camera, &GlobalTransform, &VisibleEntities), With>, rapier_context: Res, ) { if !mouse_events.is_empty() { let window = windows.single(); for _ in mouse_events.iter() { if let Some(cursor) = window.cursor_position() { for (camera, camera_t, visible_entities) in camera_q.iter() { if let Some(ray) = camera.viewport_to_world(camera_t, cursor) { if let Some((entity, toi)) = rapier_context.cast_ray( ray.origin, ray.direction, f32::MAX, true, QueryFilter::new() .predicate(&|entity| visible_entities.entities.contains(&entity)), ) { if let Some(mut cmds) = commands.get_entity(entity) { info!("hit! ({:?}@{})", entity, toi); cmds.insert(ColliderDebugColor(Color::BLUE)); } } } } } } } }