update
This commit is contained in:
parent
1e6d352e00
commit
432e5a737a
54
Cargo.lock
generated
54
Cargo.lock
generated
@ -154,6 +154,12 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||
|
||||
[[package]]
|
||||
name = "android_log-sys"
|
||||
version = "0.3.2"
|
||||
@ -1704,6 +1710,18 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"num-traits",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.8.1"
|
||||
@ -2663,6 +2681,30 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.6"
|
||||
@ -2756,6 +2798,17 @@ version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "iyes_perf_ui"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e4468c51a47d2422a3a3e01b45cb47ed0fbce520495dd7de495bcfe8ce0f856"
|
||||
dependencies = [
|
||||
"bevy",
|
||||
"chrono",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.21.1"
|
||||
@ -4204,6 +4257,7 @@ dependencies = [
|
||||
"bevy",
|
||||
"bevy-inspector-egui",
|
||||
"bevy_panorbit_camera",
|
||||
"iyes_perf_ui",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@ -7,6 +7,7 @@ edition = "2024"
|
||||
bevy = "0.16"
|
||||
bevy-inspector-egui = "0.33.1"
|
||||
bevy_panorbit_camera = "0.26"
|
||||
iyes_perf_ui = "0.5.0"
|
||||
|
||||
# Enable a small amount of optimization in the dev profile.
|
||||
[profile.dev]
|
||||
|
||||
87
examples/cam_follow.rs
Normal file
87
examples/cam_follow.rs
Normal file
@ -0,0 +1,87 @@
|
||||
//! Demonstrates how to have the camera follow a target object
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_panorbit_camera::{PanOrbitCamera, PanOrbitCameraPlugin};
|
||||
use std::f32::consts::TAU;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_plugins(PanOrbitCameraPlugin)
|
||||
.add_systems(Startup, setup)
|
||||
.add_systems(Update, (animate_cube, cam_follow).chain())
|
||||
.run();
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct Cube;
|
||||
|
||||
fn setup(
|
||||
mut commands: Commands,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
) {
|
||||
// Ground
|
||||
commands.spawn((
|
||||
Mesh3d(meshes.add(Plane3d::default().mesh().size(0.5, 0.5))),
|
||||
MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
|
||||
));
|
||||
// Cube
|
||||
commands
|
||||
.spawn((
|
||||
Mesh3d(meshes.add(Cuboid::new(0.1, 0.1, 0.1))),
|
||||
MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))),
|
||||
Transform::from_xyz(0.0, 0.5, 0.0),
|
||||
))
|
||||
.insert(Cube);
|
||||
// Light
|
||||
commands.spawn((
|
||||
PointLight {
|
||||
shadows_enabled: true,
|
||||
..default()
|
||||
},
|
||||
Transform::from_xyz(4.0, 8.0, 4.0),
|
||||
));
|
||||
// Camera
|
||||
commands.spawn((
|
||||
Transform::from_translation(Vec3::new(0.0, 0.15, 0.5)),
|
||||
PanOrbitCamera {
|
||||
// Panning the camera changes the focus, and so you most likely want to disable
|
||||
// panning when setting the focus manually
|
||||
pan_sensitivity: 0.0,
|
||||
// If you want to fully control the camera's focus, set smoothness to 0 so it
|
||||
// immediately snaps to that location. If you want the 'follow' to be smoothed,
|
||||
// leave this at default or set it to something between 0 and 1.
|
||||
pan_smoothness: 0.0,
|
||||
..default()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
/// Move the cube in a circle around the Y axis
|
||||
fn animate_cube(
|
||||
time: Res<Time>,
|
||||
mut cube_q: Query<&mut Transform, With<Cube>>,
|
||||
mut angle: Local<f32>,
|
||||
) {
|
||||
if let Ok(mut cube_tfm) = cube_q.single_mut() {
|
||||
// Rotate 20 degrees a second, wrapping around to 0 after a full rotation
|
||||
*angle += 20f32.to_radians() * time.delta_secs() % TAU;
|
||||
// Convert angle to position
|
||||
let pos = Vec3::new(angle.sin() * 0.15, 0.05, angle.cos() * 0.15);
|
||||
cube_tfm.translation = pos;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the camera's focus to the cube's position
|
||||
fn cam_follow(mut pan_orbit_q: Query<&mut PanOrbitCamera>, cube_q: Query<&Transform, With<Cube>>) {
|
||||
if let Ok(mut pan_orbit) = pan_orbit_q.single_mut() {
|
||||
if let Ok(cube_tfm) = cube_q.single() {
|
||||
pan_orbit.target_focus = cube_tfm.translation;
|
||||
// Whenever changing properties manually like this, it's necessary to force
|
||||
// PanOrbitCamera to update this frame (by default it only updates when there are
|
||||
// input events).
|
||||
pan_orbit.force_update = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
169
src/main.rs
169
src/main.rs
@ -1,11 +1,12 @@
|
||||
use bevy::{
|
||||
core_pipeline::{bloom::Bloom, tonemapping::Tonemapping},
|
||||
input::mouse::{MouseScrollUnit, MouseWheel},
|
||||
math::DVec3,
|
||||
prelude::*,
|
||||
window::WindowMode,
|
||||
window::{WindowMode, WindowResolution},
|
||||
};
|
||||
use bevy_inspector_egui::{bevy_egui::EguiPlugin, quick::WorldInspectorPlugin};
|
||||
use bevy_panorbit_camera::{PanOrbitCamera, PanOrbitCameraPlugin};
|
||||
use iyes_perf_ui::{PerfUiPlugin, prelude::PerfUiAllEntries};
|
||||
|
||||
// Scaling factor to convert AU to game units
|
||||
// Neptune is approximately 30.1 astronomical units (AU) from the Sun
|
||||
@ -55,6 +56,8 @@ struct ObjectBundle {
|
||||
|
||||
#[derive(Component)]
|
||||
struct Star;
|
||||
#[derive(Component)]
|
||||
struct Earth;
|
||||
|
||||
// Component for UI name labels
|
||||
#[derive(Component)]
|
||||
@ -66,20 +69,33 @@ struct ObjectLabel {
|
||||
#[derive(Component)]
|
||||
struct Trackable {}
|
||||
|
||||
// Resource to track which entity the camera should follow
|
||||
#[derive(Resource)]
|
||||
struct CameraFollow {
|
||||
target: Option<Entity>,
|
||||
distance: f32, // Current zoom distance from target
|
||||
}
|
||||
|
||||
pub struct SolarRenderingPlugin;
|
||||
|
||||
impl Plugin for SolarRenderingPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(Startup, (setup_rendering, setup_ui))
|
||||
.add_systems(
|
||||
FixedPostUpdate,
|
||||
(
|
||||
sync_radius_to_mesh,
|
||||
sync_position_to_transform,
|
||||
sync_name_labels,
|
||||
),
|
||||
)
|
||||
.add_systems(Update, (update_label_positions, handle_label_clicks));
|
||||
app.insert_resource(CameraFollow {
|
||||
target: None,
|
||||
distance: 10.0,
|
||||
})
|
||||
.add_systems(Startup, (setup_rendering, setup_ui))
|
||||
.add_systems(
|
||||
FixedPostUpdate,
|
||||
(
|
||||
sync_radius_to_mesh,
|
||||
sync_position_to_transform,
|
||||
sync_name_labels,
|
||||
camera_follow_system.after(sync_position_to_transform),
|
||||
),
|
||||
)
|
||||
.add_systems(PostUpdate, update_label_positions)
|
||||
.add_systems(Update, (handle_label_clicks, handle_scroll_zoom));
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,8 +109,6 @@ fn sync_radius_to_mesh(
|
||||
// Convert AU to game units for rendering
|
||||
let render_radius = radius.0.0 * AU_TO_GAME_UNITS;
|
||||
|
||||
println!("render_radius: {:?}", render_radius);
|
||||
|
||||
// Create or update sphere mesh
|
||||
let sphere_mesh = meshes.add(Sphere::new(render_radius as f32));
|
||||
let material = materials.add(if star.is_none() {
|
||||
@ -129,7 +143,6 @@ fn sync_position_to_transform(
|
||||
for (entity, position, transform) in query.iter_mut() {
|
||||
// Convert AU to game units for rendering
|
||||
let scaled_position = position.0.0 * AU_TO_GAME_UNITS;
|
||||
println!("scaled_position: {:?}", scaled_position);
|
||||
match transform {
|
||||
Some(mut t) => {
|
||||
// Update existing transform
|
||||
@ -161,13 +174,14 @@ fn setup_rendering(mut commands: Commands) {
|
||||
}),
|
||||
Tonemapping::TonyMcMapface,
|
||||
Transform::from_translation(Vec3::new(0., 0., 10.0)),
|
||||
Bloom::NATURAL,
|
||||
PanOrbitCamera {
|
||||
pan_sensitivity: 0.0, // Disable panning by setting sensitivity to 0
|
||||
focus: Vec3::ZERO,
|
||||
zoom_lower_limit: 1e-8,
|
||||
..default()
|
||||
},
|
||||
// Bloom::NATURAL,
|
||||
// PanOrbitCamera {
|
||||
// pan_sensitivity: 0.0, // Disable panning by setting sensitivity to 0
|
||||
// focus: Vec3::ZERO,
|
||||
// zoom_lower_limit: 1e-8,
|
||||
// allow_upside_down: true,
|
||||
// ..default()
|
||||
// },
|
||||
));
|
||||
|
||||
commands.spawn(PointLight {
|
||||
@ -175,6 +189,7 @@ fn setup_rendering(mut commands: Commands) {
|
||||
shadows_enabled: true,
|
||||
..default()
|
||||
});
|
||||
commands.spawn(PerfUiAllEntries::default());
|
||||
}
|
||||
|
||||
fn setup_ui(mut commands: Commands) {
|
||||
@ -225,7 +240,7 @@ fn sync_name_labels(
|
||||
|
||||
fn update_label_positions(
|
||||
mut label_query: Query<(&mut Node, &ObjectLabel, &mut Text), With<ObjectLabel>>,
|
||||
objects_query: Query<(&Position, &Name, &Radius)>,
|
||||
objects_query: Query<(&GlobalTransform, &Name, &Radius)>,
|
||||
camera_query: Query<(&Camera, &GlobalTransform)>,
|
||||
) {
|
||||
let Ok((camera, camera_transform)) = camera_query.single() else {
|
||||
@ -233,14 +248,12 @@ fn update_label_positions(
|
||||
};
|
||||
|
||||
for (mut node, label, mut text) in label_query.iter_mut() {
|
||||
if let Ok((position, name, _radius)) = objects_query.get(label.target_entity) {
|
||||
let world_pos = position.0.0 * AU_TO_GAME_UNITS;
|
||||
|
||||
if let Ok(screen_pos) = camera.world_to_viewport(camera_transform, world_pos.as_vec3())
|
||||
{
|
||||
if let Ok((global_transform, name, _radius)) = objects_query.get(label.target_entity) {
|
||||
let world_pos = global_transform.translation();
|
||||
if let Ok(screen_pos) = camera.world_to_viewport(camera_transform, world_pos) {
|
||||
// Position the label on screen, offset slightly to avoid overlapping the object
|
||||
node.left = Val::Px(screen_pos.x + 10.0);
|
||||
node.top = Val::Px(screen_pos.y - 10.0);
|
||||
node.left = Val::Px(screen_pos.x);
|
||||
node.top = Val::Px(screen_pos.y);
|
||||
|
||||
// Update text in case name changed
|
||||
text.0 = name.0.0.clone();
|
||||
@ -261,26 +274,87 @@ fn handle_label_clicks(
|
||||
(&Interaction, &ObjectLabel),
|
||||
(Changed<Interaction>, With<ObjectLabel>),
|
||||
>,
|
||||
trackable_objects: Query<(&Position, &Trackable, &Radius)>,
|
||||
mut camera_query: Query<&mut PanOrbitCamera>,
|
||||
trackable_objects: Query<(&Transform, &Trackable, &Radius)>,
|
||||
mut camera_query: Query<&mut Transform, (With<Camera>, Without<Trackable>)>,
|
||||
mut camera_follow: ResMut<CameraFollow>,
|
||||
) {
|
||||
for (interaction, label) in interaction_query.iter() {
|
||||
if *interaction == Interaction::Pressed {
|
||||
if let Ok((position, _trackable, radius)) = trackable_objects.get(label.target_entity) {
|
||||
if let Ok(mut pan_orbit) = camera_query.single_mut() {
|
||||
// Focus camera on the clicked object using target values for smooth transitions
|
||||
pan_orbit.target_focus = (position.0.0 * AU_TO_GAME_UNITS).as_vec3();
|
||||
pan_orbit.target_radius = (radius.0.0 * AU_TO_GAME_UNITS) as f32 * 4.;
|
||||
if let Ok((target_transform, _trackable, radius)) =
|
||||
trackable_objects.get(label.target_entity)
|
||||
{
|
||||
if let Ok(mut camera_transform) = camera_query.single_mut() {
|
||||
let target_world_pos = target_transform.translation;
|
||||
|
||||
// Also set the immediate values to ensure it takes effect
|
||||
// pan_orbit.focus = position.0.0.as_vec3();
|
||||
// pan_orbit.radius = Some(trackable.zoom_distance);
|
||||
// Calculate appropriate distance based on entity radius (with some padding)
|
||||
let entity_radius = (radius.0.0 * AU_TO_GAME_UNITS) as f32;
|
||||
let desired_distance = entity_radius * 16.0; // 8x radius
|
||||
|
||||
// Position camera at desired distance from target
|
||||
let camera_direction =
|
||||
(camera_transform.translation - target_world_pos).normalize();
|
||||
let new_camera_pos = target_world_pos + camera_direction * desired_distance;
|
||||
|
||||
camera_transform.translation = new_camera_pos;
|
||||
camera_transform.look_at(target_world_pos, Vec3::Y);
|
||||
|
||||
// Set the follow target and initial distance
|
||||
camera_follow.target = Some(label.target_entity);
|
||||
camera_follow.distance = desired_distance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn camera_follow_system(
|
||||
camera_follow: Res<CameraFollow>,
|
||||
objects_query: Query<&Transform, With<Trackable>>,
|
||||
mut camera_query: Query<&mut Transform, (With<Camera>, Without<Trackable>)>,
|
||||
) {
|
||||
if let Some(target_entity) = camera_follow.target {
|
||||
if let Ok(target_transform) = objects_query.get(target_entity) {
|
||||
if let Ok(mut camera_transform) = camera_query.single_mut() {
|
||||
// Fixed camera direction relative to tracked object (stationary relative position)
|
||||
let fixed_direction = Vec3::new(0.0, 0.0, 1.0);
|
||||
|
||||
// Position camera at the specified distance from target in fixed direction
|
||||
let new_camera_pos =
|
||||
target_transform.translation + fixed_direction * camera_follow.distance;
|
||||
camera_transform.translation = new_camera_pos;
|
||||
|
||||
// Keep camera looking at the target entity
|
||||
camera_transform.look_at(target_transform.translation, Vec3::Y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_scroll_zoom(
|
||||
mut scroll_events: EventReader<MouseWheel>,
|
||||
mut camera_follow: ResMut<CameraFollow>,
|
||||
) {
|
||||
for event in scroll_events.read() {
|
||||
let zoom_delta = match event.unit {
|
||||
MouseScrollUnit::Line => event.y * 0.1,
|
||||
MouseScrollUnit::Pixel => event.y * 0.01,
|
||||
};
|
||||
|
||||
// Update zoom distance with limits
|
||||
camera_follow.distance = (camera_follow.distance * (1.0 - zoom_delta)).clamp(1e-9, 100.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn initialize_camera_follow(
|
||||
earth_query: Query<Entity, (With<Earth>, With<Trackable>)>,
|
||||
mut camera_follow: ResMut<CameraFollow>,
|
||||
) {
|
||||
if let Ok(earth) = earth_query.single() {
|
||||
camera_follow.target = Some(earth);
|
||||
camera_follow.distance = 1e-3;
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_solar_system(mut commands: Commands) {
|
||||
// Sun - From NASA JPL Horizons data (J2000.0 epoch)
|
||||
// Physical properties: Mass = 1988410 x 10^24 kg, Radius = 695700 km = 0.00465 AU
|
||||
@ -316,6 +390,7 @@ fn setup_solar_system(mut commands: Commands) {
|
||||
radius: Radius(DistanceAu(0.0000426)), // Mean radius in AU (6371.01 km / 149597870.691)
|
||||
},
|
||||
Trackable {},
|
||||
Earth,
|
||||
));
|
||||
|
||||
// Moon - From NASA JPL Horizons data (A.D. 2000-Jan-01 12:00:00.0000 TDB)
|
||||
@ -415,15 +490,23 @@ fn main() {
|
||||
primary_window: Some(Window {
|
||||
title: "Solar Sim".to_string(),
|
||||
mode: WindowMode::BorderlessFullscreen(MonitorSelection::Primary),
|
||||
resolution: WindowResolution::default().with_scale_factor_override(2.0),
|
||||
..default()
|
||||
}),
|
||||
..default()
|
||||
}))
|
||||
.add_plugins(PanOrbitCameraPlugin)
|
||||
.add_plugins(EguiPlugin::default())
|
||||
.add_plugins(WorldInspectorPlugin::new())
|
||||
.add_plugins(SolarRenderingPlugin)
|
||||
.add_systems(Startup, setup_solar_system)
|
||||
.add_plugins(SolarRenderingPlugin) // we want Bevy to measure these values for us:
|
||||
.add_plugins(bevy::diagnostic::FrameTimeDiagnosticsPlugin::default())
|
||||
.add_plugins(bevy::diagnostic::EntityCountDiagnosticsPlugin)
|
||||
.add_plugins(bevy::diagnostic::SystemInformationDiagnosticsPlugin)
|
||||
.add_plugins(bevy::render::diagnostic::RenderDiagnosticsPlugin)
|
||||
.add_plugins(PerfUiPlugin)
|
||||
.add_systems(
|
||||
Startup,
|
||||
(setup_solar_system, initialize_camera_follow).chain(),
|
||||
)
|
||||
.add_systems(FixedUpdate, n_body)
|
||||
.run();
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user