Skip to content

Commit d3b2cc1

Browse files
committed
feat: add interactive fly camera to gen 3D window
WASD movement, right-click + drag to look, Space/Shift for up/down, and scroll wheel to adjust speed. Coexists with the agent's gen_set_camera tool — user can explore from wherever the agent positions the camera.
1 parent 00c90f7 commit d3b2cc1

1 file changed

Lines changed: 122 additions & 1 deletion

File tree

src/gen3d/plugin.rs

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! Bevy GenPlugin — command processing, default scene, screenshot capture.
22
3+
use bevy::input::mouse::{MouseMotion, MouseWheel};
34
use bevy::prelude::*;
45
use bevy::render::mesh::Indices;
56
use bevy::render::render_asset::RenderAssetUsages;
@@ -35,6 +36,26 @@ struct PendingScreenshot {
3536
path: Option<String>,
3637
}
3738

39+
/// Marker component for the interactive fly camera.
40+
#[derive(Component)]
41+
struct FlyCam;
42+
43+
/// Configuration for the fly camera controller.
44+
#[derive(Resource)]
45+
struct FlyCamConfig {
46+
move_speed: f32,
47+
look_sensitivity: f32,
48+
}
49+
50+
impl Default for FlyCamConfig {
51+
fn default() -> Self {
52+
Self {
53+
move_speed: 5.0,
54+
look_sensitivity: 0.003,
55+
}
56+
}
57+
}
58+
3859
/// Plugin that sets up the Gen 3D environment.
3960
pub struct GenPlugin {
4061
pub channels: GenChannels,
@@ -55,8 +76,18 @@ pub fn setup_gen_app(app: &mut App, channels: GenChannels) {
5576
app.insert_resource(GenChannelRes::new(channels))
5677
.init_resource::<NameRegistry>()
5778
.init_resource::<PendingScreenshots>()
79+
.init_resource::<FlyCamConfig>()
5880
.add_systems(Startup, setup_default_scene)
59-
.add_systems(Update, (process_gen_commands, process_pending_screenshots));
81+
.add_systems(
82+
Update,
83+
(
84+
process_gen_commands,
85+
process_pending_screenshots,
86+
fly_cam_movement,
87+
fly_cam_look,
88+
fly_cam_scroll_speed,
89+
),
90+
);
6091
}
6192

6293
/// Default scene: ground plane, camera, directional light, ambient light.
@@ -91,6 +122,7 @@ fn setup_default_scene(
91122
Camera3d::default(),
92123
Transform::from_translation(Vec3::new(5.0, 5.0, 5.0)).looking_at(Vec3::ZERO, Vec3::Y),
93124
Name::new("main_camera"),
125+
FlyCam,
94126
GenEntity {
95127
entity_type: GenEntityType::Camera,
96128
},
@@ -809,3 +841,92 @@ fn handle_spawn_mesh(
809841
entity_id,
810842
}
811843
}
844+
845+
// ---------------------------------------------------------------------------
846+
// Fly camera systems
847+
// ---------------------------------------------------------------------------
848+
849+
/// WASD + Space/Shift movement relative to camera orientation.
850+
fn fly_cam_movement(
851+
keys: Res<ButtonInput<KeyCode>>,
852+
time: Res<Time>,
853+
config: Res<FlyCamConfig>,
854+
mut query: Query<&mut Transform, With<FlyCam>>,
855+
) {
856+
let Ok(mut transform) = query.get_single_mut() else {
857+
return;
858+
};
859+
860+
let forward = transform.forward().as_vec3();
861+
let right = transform.right().as_vec3();
862+
863+
let mut velocity = Vec3::ZERO;
864+
if keys.pressed(KeyCode::KeyW) {
865+
velocity += forward;
866+
}
867+
if keys.pressed(KeyCode::KeyS) {
868+
velocity -= forward;
869+
}
870+
if keys.pressed(KeyCode::KeyA) {
871+
velocity -= right;
872+
}
873+
if keys.pressed(KeyCode::KeyD) {
874+
velocity += right;
875+
}
876+
if keys.pressed(KeyCode::Space) {
877+
velocity += Vec3::Y;
878+
}
879+
if keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight) {
880+
velocity -= Vec3::Y;
881+
}
882+
883+
if velocity != Vec3::ZERO {
884+
transform.translation += velocity.normalize() * config.move_speed * time.delta_secs();
885+
}
886+
}
887+
888+
/// Right-click + mouse drag to rotate the camera (yaw and pitch).
889+
fn fly_cam_look(
890+
mouse: Res<ButtonInput<MouseButton>>,
891+
config: Res<FlyCamConfig>,
892+
mut motion_reader: EventReader<MouseMotion>,
893+
mut query: Query<&mut Transform, With<FlyCam>>,
894+
) {
895+
let delta: Vec2 = motion_reader.read().map(|e| e.delta).sum();
896+
if delta == Vec2::ZERO || !mouse.pressed(MouseButton::Right) {
897+
return;
898+
}
899+
900+
let Ok(mut transform) = query.get_single_mut() else {
901+
return;
902+
};
903+
904+
let yaw = -delta.x * config.look_sensitivity;
905+
let pitch = -delta.y * config.look_sensitivity;
906+
907+
// Apply yaw (rotate around global Y axis)
908+
transform.rotate_y(yaw);
909+
910+
// Apply pitch (rotate around local X axis) with clamping
911+
let right = transform.right().as_vec3();
912+
let new_rotation = Quat::from_axis_angle(right, pitch) * transform.rotation;
913+
914+
// Clamp pitch: check the angle between the camera's forward and the horizontal plane
915+
let new_forward = new_rotation * Vec3::NEG_Z;
916+
let pitch_angle = new_forward.y.asin();
917+
let max_pitch = 89.0_f32.to_radians();
918+
919+
if pitch_angle.abs() < max_pitch {
920+
transform.rotation = new_rotation;
921+
}
922+
}
923+
924+
/// Scroll wheel adjusts movement speed.
925+
fn fly_cam_scroll_speed(
926+
mut scroll_reader: EventReader<MouseWheel>,
927+
mut config: ResMut<FlyCamConfig>,
928+
) {
929+
for event in scroll_reader.read() {
930+
config.move_speed = (config.move_speed * (1.0 + event.y * 0.1)).clamp(0.5, 100.0);
931+
}
932+
}

0 commit comments

Comments
 (0)