diff --git a/Cargo.lock b/Cargo.lock index 93acc5e..5e3e6e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -661,16 +661,6 @@ dependencies = [ "nohash-hasher", ] -[[package]] -name = "egui-gizmo" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f732ad247afe275d6cf901e0f134025ad735007c8f4d82e667a6871f1b4a5441" -dependencies = [ - "egui", - "glam 0.24.2", -] - [[package]] name = "egui-winit" version = "0.23.0" @@ -1783,7 +1773,6 @@ version = "0.1.0" dependencies = [ "blade", "egui", - "egui-gizmo", "egui-winit", "env_logger", "log", diff --git a/Cargo.toml b/Cargo.toml index e33bc19..9df0256 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,6 @@ edition = "2021" [dependencies] blade = { path = "../blade" } egui = "0.23" -egui-gizmo = "0.12" egui-winit = "0.23" env_logger = "0.10" nalgebra = { version = "0.32", features = ["mint"] } diff --git a/README.md b/README.md new file mode 100644 index 0000000..6669ace --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# RayCraft + +A simple vehicle simulator based on [Blade engine](https://github.com/kvark/blade). + +![colliders](etc/collider-boxes.jpg) diff --git a/data/config.ron b/data/config.ron index 3225613..f9a6120 100644 --- a/data/config.ron +++ b/data/config.ron @@ -17,6 +17,7 @@ colliders: [ ( mass: 100000.0, + friction: 1.0, pos: (0, -1, 0), shape: Cuboid( half: (100000, 1, 100000), @@ -29,6 +30,7 @@ distance: 5, azimuth: 0, altitude: 0.5, + speed: 0.0, target: (0, 1, 0), fov: 1.0, ), diff --git a/data/vehicles/raceFuture.ron b/data/vehicles/raceFuture.ron index abf12ce..6f47596 100644 --- a/data/vehicles/raceFuture.ron +++ b/data/vehicles/raceFuture.ron @@ -19,6 +19,7 @@ ), collider: ( mass: 1.0, + friction: 2.0, pos: (0, 0, 0), rot: (0, 0, 90), shape: Cylinder( @@ -34,8 +35,8 @@ z: 0.5, steer: ( max_angle: 30, - stiffness: 100, - damping: 0, + stiffness: 1000, + damping: 1, ), ), ( diff --git a/etc/collider-boxes.jpg b/etc/collider-boxes.jpg new file mode 100644 index 0000000..4b90b78 Binary files /dev/null and b/etc/collider-boxes.jpg differ diff --git a/src/main.rs b/src/main.rs index 6bba89a..27d1076 100644 --- a/src/main.rs +++ b/src/main.rs @@ -43,6 +43,7 @@ struct CameraConfig { azimuth: f32, altitude: f32, distance: f32, + speed: f32, target: [f32; 3], fov: f32, } @@ -56,8 +57,9 @@ struct GameConfig { } struct Wheel { - _object: blade::ObjectHandle, + object: blade::ObjectHandle, joint: blade::JointHandle, + local_rotation: nalgebra::UnitQuaternion, } struct WheelAxle { @@ -76,7 +78,8 @@ struct Game { // engine stuff engine: blade::Engine, last_physics_update: time::Instant, - is_paused: Option, + last_camera_base_quat: nalgebra::UnitQuaternion, + is_paused: bool, // windowing window: winit::window::Window, egui_state: egui_winit::State, @@ -125,7 +128,7 @@ impl Game { let mut vehicle = Vehicle { body_handle: engine.add_object( &body_config, - nalgebra::Isometry3::translation(init_pos.x, init_pos.y, init_pos.z), + nalgebra::Isometry3::new(init_pos, nalgebra::Vector3::zeros()), blade::BodyType::Dynamic, ), drive_factor: veh_config.drive_factor, @@ -136,6 +139,7 @@ impl Game { visuals: vec![veh_config.wheel.visual], colliders: vec![veh_config.wheel.collider], }; + //Note: in the vehicle coordinate system X=left, Y=up, Z=forward for ac in veh_config.axles { let offset_left = nalgebra::Vector3::new(ac.x, 0.0, ac.z); let offset_right = nalgebra::Vector3::new(-ac.x, 0.0, ac.z); @@ -154,6 +158,7 @@ impl Game { blade::BodyType::Dynamic, ); + let joint_kind = blade::JointKind::Soft; let has_steer = ac.steer.max_angle > 0.0; let max_angle = ac.steer.max_angle.to_radians(); let locked_axes = if has_steer { @@ -162,15 +167,16 @@ impl Game { } else { rapier3d::dynamics::JointAxesMask::LOCKED_REVOLUTE_AXES }; + + let left_frame = + nalgebra::Isometry3::rotation(nalgebra::Vector3::y_axis().scale(consts::PI)); let joint_left = engine.add_joint( vehicle.body_handle, wheel_left, rapier3d::dynamics::GenericJointBuilder::new(locked_axes) .contacts_enabled(false) .local_anchor1(offset_left.into()) - .local_frame2(nalgebra::Isometry3::rotation( - nalgebra::Vector3::y_axis().scale(consts::PI), - )) + .local_frame2(left_frame) .limits(rapier3d::dynamics::JointAxis::AngY, [-max_angle, max_angle]) .motor_position(rapier3d::dynamics::JointAxis::AngX, 0.0, 1.0, 1.0) .motor_position( @@ -180,6 +186,7 @@ impl Game { ac.steer.damping, ) .build(), + joint_kind, ); let joint_right = engine.add_joint( vehicle.body_handle, @@ -196,16 +203,19 @@ impl Game { ac.steer.damping, ) .build(), + joint_kind, ); vehicle.wheel_axles.push(WheelAxle { left: Wheel { - _object: wheel_left, + object: wheel_left, joint: joint_left, + local_rotation: left_frame.rotation, }, right: Wheel { - _object: wheel_right, + object: wheel_right, joint: joint_right, + local_rotation: nalgebra::UnitQuaternion::default(), }, steer: if has_steer { Some(ac.steer) } else { None }, }); @@ -214,7 +224,8 @@ impl Game { Self { engine, last_physics_update: time::Instant::now(), - is_paused: None, + last_camera_base_quat: Default::default(), + is_paused: false, window, egui_state: egui_winit::State::new(event_loop), egui_context: egui::Context::default(), @@ -229,10 +240,10 @@ impl Game { } fn set_velocity(&mut self, velocity: f32) { + self.engine.wake_up(self.vehicle.body_handle); for wax in self.vehicle.wheel_axles.iter() { for &joint_handle in &[wax.left.joint, wax.right.joint] { - let joint = self.engine.get_joint_mut(joint_handle); - joint.data.set_motor_velocity( + self.engine[joint_handle].set_motor_velocity( rapier3d::dynamics::JointAxis::AngX, velocity, self.vehicle.drive_factor, @@ -248,8 +259,7 @@ impl Game { None => continue, }; for &joint_handle in &[wax.left.joint, wax.right.joint] { - let joint = self.engine.get_joint_mut(joint_handle); - joint.data.set_motor_position( + self.engine[joint_handle].set_motor_position( rapier3d::dynamics::JointAxis::AngY, angle_rad, steer.stiffness, @@ -259,6 +269,34 @@ impl Game { } } + /// When front wheels are steered, their axis changes. + /// This routine re-aligns the axis of rotation every frame. + fn align_wheels(&mut self) { + let veh_isometry = self.engine.get_object_isometry(self.vehicle.body_handle); + let veh_y_axis = veh_isometry.transform_vector(&nalgebra::Vector3::y_axis()); + for wax in self.vehicle.wheel_axles.iter() { + if wax.steer.is_none() { + continue; + } + for &wheel in &[&wax.left, &wax.right] { + let w_isometry = *self.engine.get_object_isometry(wheel.object); + let mut joint = &mut self.engine[wheel.joint]; + let veh_y_in_wheel_frame = w_isometry.inverse().transform_vector(&veh_y_axis); + let x_angle = veh_y_in_wheel_frame.z.atan2(veh_y_in_wheel_frame.y); + let sign = wheel + .local_rotation + .transform_vector(&nalgebra::Vector3::x_axis()) + .x + .signum(); + let local_rot = nalgebra::UnitQuaternion::from_axis_angle( + &nalgebra::Vector3::x_axis(), + sign * x_angle, + ); + joint.local_frame2.rotation = wheel.local_rotation * local_rot; + } + } + } + fn on_event(&mut self, event: &winit::event::WindowEvent) -> bool { let response = self.egui_state.on_event(&self.egui_context, event); if response.consumed { @@ -324,64 +362,6 @@ impl Game { false } - fn add_camera_manipulation(&mut self, ui: &mut egui::Ui) { - let mode = match self.is_paused { - Some(mode) => mode, - None => return, - }; - - let cc = &self.cam_config; - let eye_dir = nalgebra::Vector3::new( - -cc.azimuth.sin() * cc.altitude.cos(), - cc.altitude.sin(), - -cc.azimuth.cos() * cc.altitude.cos(), - ); - - let rotation = { - let z = eye_dir; - //let x = z.cross(&nalgebra::Vector3::y_axis()).normalize(); - let x = nalgebra::Vector3::new(cc.azimuth.cos(), 0.0, -cc.azimuth.sin()); - //let y = z.cross(&x); - let y = nalgebra::Vector3::new( - cc.altitude.sin() * -cc.azimuth.sin(), - -cc.altitude.cos(), - cc.altitude.sin() * -cc.azimuth.cos(), - ); - nalgebra::geometry::UnitQuaternion::from_rotation_matrix( - &nalgebra::geometry::Rotation3::from_basis_unchecked(&[x, y, z]).transpose(), - ) - }; - let view = { - let t = rotation * (nalgebra::Vector3::from(cc.target) - eye_dir.scale(cc.distance)); - nalgebra::geometry::Isometry3::from_parts(t.into(), rotation) - }; - - let aspect = self.engine.screen_aspect(); - let depth_range = 1.0f32..10000.0; //TODO? - let projection_matrix = - nalgebra::Matrix4::new_perspective(aspect, cc.fov, depth_range.start, depth_range.end); - - let gizmo = egui_gizmo::Gizmo::new("Object") - .model_matrix(view.to_homogeneous()) - .projection_matrix(projection_matrix) - .mode(mode) - .orientation(egui_gizmo::GizmoOrientation::Global) - .snapping(true); - - if let Some(result) = gizmo.interact(ui) { - let q = nalgebra::Unit::new_normalize(nalgebra::Quaternion::from( - result.rotation.to_array(), - )); - let m = q.inverse().to_rotation_matrix(); - self.cam_config.azimuth = -m[(2, 0)].atan2(m[(0, 0)]); - self.cam_config.altitude = (-m[(1, 1)]).acos(); - let t_local = q - .inverse() - .transform_vector(&nalgebra::Vector3::from(result.translation.to_array())); - self.cam_config.target = (t_local + eye_dir.scale(self.cam_config.distance)).into(); - } - } - fn populate_hud(&mut self, ui: &mut egui::Ui) { egui::CollapsingHeader::new("Camera") .default_open(true) @@ -392,21 +372,13 @@ impl Game { .clamp_range(1.0..=100.0), ); ui.horizontal(|ui| { - ui.selectable_value( - &mut self.is_paused, - Some(egui_gizmo::GizmoMode::Translate), - "Target", - ); + ui.label("Target"); ui.add(egui::DragValue::new(&mut self.cam_config.target[1])); ui.add(egui::DragValue::new(&mut self.cam_config.target[2])); }); ui.horizontal(|ui| { let eps = 0.01; - ui.selectable_value( - &mut self.is_paused, - Some(egui_gizmo::GizmoMode::Rotate), - "Angle", - ); + ui.label("Angle"); ui.add( egui::DragValue::new(&mut self.cam_config.azimuth) .clamp_range(-consts::FRAC_PI_2..=consts::FRAC_PI_2) @@ -419,12 +391,9 @@ impl Game { ); }); ui.add(egui::Slider::new(&mut self.cam_config.fov, 0.5f32..=2.0f32).text("FOV")); - if self.is_paused.is_some() && ui.button("Unpause").clicked() { - self.is_paused = None; - } + ui.toggle_value(&mut self.is_paused, "Pause"); }); - self.add_camera_manipulation(ui); self.engine.populate_hud(ui); } @@ -455,7 +424,8 @@ impl Game { ); let engine_dt = self.last_physics_update.elapsed().as_secs_f32(); self.last_physics_update = time::Instant::now(); - if self.is_paused.is_none() { + if !self.is_paused { + self.align_wheels(); self.engine.update(engine_dt); } @@ -470,12 +440,19 @@ impl Game { let base_quat = nalgebra::UnitQuaternion::new_normalize(nalgebra::Quaternion::from_parts( veh_isometry.rotation.quaternion().w, - nalgebra::Vector3::y_axis().scale(projection.abs()), + nalgebra::Vector3::y_axis().scale(projection), )); + + let cc = &self.cam_config; + let smooth_t = (-engine_dt * cc.speed).exp(); + let smooth_quat = nalgebra::UnitQuaternion::new_normalize( + base_quat.lerp(&self.last_camera_base_quat, smooth_t), + ); let base = - nalgebra::geometry::Isometry3::from_parts(veh_isometry.translation, base_quat); + nalgebra::geometry::Isometry3::from_parts(veh_isometry.translation, smooth_quat); + self.last_camera_base_quat = smooth_quat; + //TODO: `nalgebra::Point3::from(mint::Vector3)` doesn't exist? - let cc = &self.cam_config; let source = nalgebra::Vector3::from(cc.target) + nalgebra::Vector3::new( -cc.azimuth.sin() * cc.altitude.cos(),