In the last article we were able to render rather large grids. In most games with large grids, you can zoom in and out of the grid, rotate the camera, and move the camera around. At this point, we should be able to render a grid that looks like the one below. If you don’t see this on your screen, then download the code for the previous post and start from there.
Camera Requirements
There are a few requirements we have for this kind of camera, let’s list them out here.
I should be able to move the camera left to right, and forward to backward on the grid using WASD or the arrow keys.
I should be able to zoom in and zoom out of the grid using the mouse scroll wheel or the + and - keys.
As the camera zooms in and out of the grid, it should stay focused on the same point.
As the camera zooms in and out of the grid, it should become more parallel with the grid, instead of looking directly at the top of the grid.
I should be able to rotate the camera left to right using the Q and E keys.
Building the Camera
We already have a camera in the scene that we have positioned manually.
We see that it is quite easy to configure the camera’s position and have it look at a specific point. What we want to do is change the rotation of the camera relative to the plane while zooming in and out as well as allow for the player to change what the camera is looking at using directional keys. In order to model this, we will need to store some data in a new Bevy Component. We will call our new component CameraTarget.
#[derive(Debug, Component)]pubstructCameraTarget{/// which offset to use for the camera
zoom_level: usize,/// list of offsets to position the camera relative to the look_at point
zoom_level_offsets: Vec<Vec3>,/// point in space the camera should look at
look_at: Vec3,/// rotation angle around the up axis
rotation: f32,/// normal vector representing up for the camera
up: Vec3,/// true wheh zoom_level, look_at, or up change
/// this let's our system know to update the camera transform in the scene
is_dirty: bool,}implDefaultforCameraTarget{fndefault()-> Self{Self{zoom_level: 0,zoom_level_offsets: vec![],look_at: Vec3::default(),rotation: 0.0,up: Vec3::Y,is_dirty: true,}}}
zoom_level tells us how zoomed in the camera should be, let’s say lower values are zoomed in, while higher values are zoomed out. This is an index into the zoom_level_offsets vector.
zoom_level_offsets these are vectors that describe where to position the camera relative to the look_at position for all of the zoom levels we have defined.
look_at is the point on the grid that we want the camera to look at.
rotation is the rotation in radians around the up axis.
up is the unit vector representing up for the camera.
is_dirty is a flag that gets set to true whenever we need to update the camera transform in the scene. The camera transform describes how to rotate and position the camera in the scene.
Let’s implement some methods to help us work with our new CameraTarget struct.
implTextGem{...fnstartup(mutcommands: Commands,mutmeshes: ResMut<Assets<Mesh>>,mutgrid_materials: ResMut<Assets<ExtendedMaterial<StandardMaterial,GridMaterial>>>,){// Setup CameraTarget
letmutcamera_target=CameraTarget::default().looking_at(Vec3::new(0.0,0.0,0.0)).with_up(Vec3::Y);letmin_x=1000.0;letmax_x=1200.0;// move further away from the look_at point as we zoom out
letmin_y=500.0;letmax_y=4000.0;// zoom out by this much at the maximum zoom level
letnum_steps=100;// we will have 100 zoom levels
foriin0..num_steps{letprogress=iasf32/num_stepsasf32;lety=min_y+(max_y-min_y)*progress;letx=min_x+(max_x-min_x)*progress;camera_target.add_zoom_level_offset(Vec3::new(x,y,0.0))}// Camera
commands.spawn((Camera3dBundle::default(),camera_target));}...}
If you run the application now with cargo run you will notice that we are rendering a blank screen. This is because we aren’t actually positioning the Camera at our CameraTarget.
Update Camera Position
In order to keep the scene Camera in sync with our CameraTarget, we need to create an update function that runs every frame.
// Update use statements
usestd::f32::consts::PI;usebevy::{input::mouse::MouseWheel,pbr::{ExtendedMaterial,MaterialExtension},prelude::*,render::{mesh::Indices,render_resource::{AsBindGroup,PrimitiveTopology,ShaderRef},},};...implPluginforTextGem{fnbuild(&self,app: &mutApp){letmutmaterial_plugin=MaterialPlugin::<ExtendedMaterial<StandardMaterial,GridMaterial>>::default();material_plugin.prepass_enabled=false;app.add_plugins(material_plugin).add_systems(Startup,Self::hello_world).add_systems(Startup,Self::startup).add_systems(Update,CameraTarget::update);}}...implCameraTarget{pubfnupdate(mutscroll_evr: EventReader<MouseWheel>,keys: Res<Input<KeyCode>>,mutcamera_query: Query<(&mutCameraTarget,&mutTransform),With<Camera>>,){let(muttarget,mutcamera_transform)=camera_query.single_mut();letdelta_y: f32=scroll_evr.read().map(|ev|ev.y).sum();letmutdelta_zoom_level=ifdelta_y<0.0{-1}elseifdelta_y>0.0{1}else{0};ifkeys.pressed(KeyCode::Equals)||keys.pressed(KeyCode::Plus){delta_zoom_level=-1;}ifkeys.pressed(KeyCode::Minus){delta_zoom_level=1;}letmutdelta_rotation: f32=0.0;ifkeys.pressed(KeyCode::Q){delta_rotation=-0.005;}ifkeys.pressed(KeyCode::E){delta_rotation=0.005;}letmutdelta_x: f32=0.0;letmutdelta_z: f32=0.0;ifkeys.pressed(KeyCode::W)||keys.pressed(KeyCode::Up){delta_x=1.0;}ifkeys.pressed(KeyCode::S)||keys.pressed(KeyCode::Down){delta_x=-1.0;}ifkeys.pressed(KeyCode::D)||keys.pressed(KeyCode::Right){delta_z=-1.0;}ifkeys.pressed(KeyCode::A)||keys.pressed(KeyCode::Left){delta_z=1.0;}ifdelta_zoom_level!=0{target.change_zoom_to(delta_zoom_level);}ifdelta_rotation!=0.0{target.change_rotation(delta_rotation);}target.update_transform(&mutcamera_transform);ifdelta_x!=0.0||delta_z!=0.0{letmask=Vec3::new(1.0,1.0,1.0)-target.get_up();letlook_at=target.get_look_at()*mask;// multiply out the up component
letcamera_at=camera_transform.translation*mask;// multiply out the up component
letforward=(look_at-camera_at).normalize();letmutright_rotation=Transform::from_xyz(0.0,0.0,0.0);right_rotation.rotate_around(Vec3::default(),Quat::from_axis_angle(target.up,PI/2.0));letright=right_rotation*forward;letnew_look_at=look_at+(forward*delta_x*10.0)+(right*delta_z*10.0);target.look_at(new_look_at);target.update_transform(&mutcamera_transform);}}...}
Run the application again with cargo run and you should be able to move the camera around and zoom in/out using the controls we have configured!
Next Up…
This is a pretty good first pass and fulfills all of our requirements! However, there is still a major issue with it that we will need to fix up. The issue is that the speed of moving the camera is tied to the frame rate. In order to fix this, we will need to incorporate the time delta between frames into our calculations. We’ll take care of this in the next post before moving on to rendering text to our grid!
Full code for this post can be found here: TextGem Post 4.