mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'woeful/shoulder_aiming_camera' into 'master'
Third person over-the-shoulder camera and look_dir calculations when aiming See merge request veloren/veloren!4285
This commit is contained in:
commit
2c9ceb51d5
@ -57,6 +57,8 @@ hud-settings-walking_speed_behavior = Walking speed behavior
|
||||
hud-settings-walking_speed = Walking speed
|
||||
hud-settings-camera_clamp_behavior = Camera clamp behavior
|
||||
hud-settings-zoom_lock_behavior = Camera zoom lock behavior
|
||||
hud-settings-aim_offset_x = Horizontal Aim Offset
|
||||
hud-settings-aim_offset_y = Vertical Aim Offset
|
||||
hud-settings-player_physics_behavior = Player physics (experimental)
|
||||
hud-settings-stop_auto_walk_on_input = Stop auto walk on movement
|
||||
hud-settings-auto_camera = Auto camera
|
||||
|
@ -2200,6 +2200,43 @@ fn closest_points(n: LineSegment2<f32>, m: LineSegment2<f32>) -> (Vec2<f32>, Vec
|
||||
}
|
||||
}
|
||||
|
||||
// Get closest point between 2 3D line segments https://math.stackexchange.com/a/4289668
|
||||
pub fn closest_points_3d(n: LineSegment3<f32>, m: LineSegment3<f32>) -> (Vec3<f32>, Vec3<f32>) {
|
||||
let p1 = n.start;
|
||||
let p2 = n.end;
|
||||
let p3 = m.start;
|
||||
let p4 = m.end;
|
||||
|
||||
let d1 = p2 - p1;
|
||||
let d2 = p4 - p3;
|
||||
let d21 = p3 - p1;
|
||||
|
||||
let v22 = d2.dot(d2);
|
||||
let v11 = d1.dot(d1);
|
||||
let v21 = d2.dot(d1);
|
||||
let v21_1 = d21.dot(d1);
|
||||
let v21_2 = d21.dot(d2);
|
||||
|
||||
let denom = v21 * v21 - v22 * v11;
|
||||
|
||||
let (s, t) = if denom == 0.0 {
|
||||
let s = 0.0;
|
||||
let t = (v11 * s - v21_1) / v21;
|
||||
(s, t)
|
||||
} else {
|
||||
let s = (v21_2 * v21 - v22 * v21_1) / denom;
|
||||
let t = (-v21_1 * v21 + v11 * v21_2) / denom;
|
||||
(s, t)
|
||||
};
|
||||
|
||||
let (s, t) = (s.clamp(0.0, 1.0), t.clamp(0.0, 1.0));
|
||||
|
||||
let p_a = p1 + s * d1;
|
||||
let p_b = p3 + t * d2;
|
||||
|
||||
(p_a, p_b)
|
||||
}
|
||||
|
||||
/// Find pushback vector and collision_distance we assume between this
|
||||
/// colliders assuming that only one of them is capsule prism.
|
||||
fn capsule2cylinder(c0: ColliderContext, c1: ColliderContext) -> (Vec2<f32>, f32) {
|
||||
|
@ -59,6 +59,12 @@ widget_ids! {
|
||||
bow_zoom_label,
|
||||
zoom_lock_button,
|
||||
zoom_lock_label,
|
||||
aim_offset_x_slider,
|
||||
aim_offset_x_label,
|
||||
aim_offset_x_value,
|
||||
aim_offset_y_slider,
|
||||
aim_offset_y_label,
|
||||
aim_offset_y_value,
|
||||
}
|
||||
}
|
||||
|
||||
@ -662,12 +668,78 @@ impl<'a> Widget for Gameplay<'a> {
|
||||
.color(TEXT_COLOR)
|
||||
.set(state.ids.zoom_lock_label, ui);
|
||||
|
||||
// Aim offset x
|
||||
let display_aim_offset_x = self.global_state.settings.gameplay.aim_offset_x;
|
||||
Text::new(&self.localized_strings.get_msg("hud-settings-aim_offset_x"))
|
||||
.down_from(state.ids.zoom_lock_behavior_list, 10.0)
|
||||
.font_size(self.fonts.cyri.scale(14))
|
||||
.font_id(self.fonts.cyri.conrod_id)
|
||||
.color(TEXT_COLOR)
|
||||
.set(state.ids.aim_offset_x_label, ui);
|
||||
|
||||
if let Some(new_val) = ImageSlider::continuous(
|
||||
display_aim_offset_x,
|
||||
-3.0,
|
||||
3.0,
|
||||
self.imgs.slider_indicator,
|
||||
self.imgs.slider,
|
||||
)
|
||||
.w_h(550.0, 22.0)
|
||||
.down_from(state.ids.aim_offset_x_label, 10.0)
|
||||
.track_breadth(30.0)
|
||||
.slider_length(10.0)
|
||||
.pad_track((5.0, 5.0))
|
||||
.set(state.ids.aim_offset_x_slider, ui)
|
||||
{
|
||||
events.push(AdjustAimOffsetX(new_val));
|
||||
}
|
||||
|
||||
Text::new(&format!("{:.2}", display_aim_offset_x))
|
||||
.right_from(state.ids.aim_offset_x_slider, 8.0)
|
||||
.font_size(self.fonts.cyri.scale(14))
|
||||
.font_id(self.fonts.cyri.conrod_id)
|
||||
.color(TEXT_COLOR)
|
||||
.set(state.ids.aim_offset_x_value, ui);
|
||||
|
||||
// Aim offset y
|
||||
let display_aim_offset_y = self.global_state.settings.gameplay.aim_offset_y;
|
||||
Text::new(&self.localized_strings.get_msg("hud-settings-aim_offset_y"))
|
||||
.down_from(state.ids.aim_offset_x_slider, 10.0)
|
||||
.font_size(self.fonts.cyri.scale(14))
|
||||
.font_id(self.fonts.cyri.conrod_id)
|
||||
.color(TEXT_COLOR)
|
||||
.set(state.ids.aim_offset_y_label, ui);
|
||||
|
||||
if let Some(new_val) = ImageSlider::continuous(
|
||||
display_aim_offset_y,
|
||||
-3.0,
|
||||
3.0,
|
||||
self.imgs.slider_indicator,
|
||||
self.imgs.slider,
|
||||
)
|
||||
.w_h(550.0, 22.0)
|
||||
.down_from(state.ids.aim_offset_y_label, 10.0)
|
||||
.track_breadth(30.0)
|
||||
.slider_length(10.0)
|
||||
.pad_track((5.0, 5.0))
|
||||
.set(state.ids.aim_offset_y_slider, ui)
|
||||
{
|
||||
events.push(AdjustAimOffsetY(new_val));
|
||||
}
|
||||
|
||||
Text::new(&format!("{:.2}", display_aim_offset_y))
|
||||
.right_from(state.ids.aim_offset_y_slider, 8.0)
|
||||
.font_size(self.fonts.cyri.scale(14))
|
||||
.font_id(self.fonts.cyri.conrod_id)
|
||||
.color(TEXT_COLOR)
|
||||
.set(state.ids.aim_offset_y_value, ui);
|
||||
|
||||
// Reset the gameplay settings to the default settings
|
||||
if Button::image(self.imgs.button)
|
||||
.w_h(RESET_BUTTONS_WIDTH, RESET_BUTTONS_HEIGHT)
|
||||
.hover_image(self.imgs.button_hover)
|
||||
.press_image(self.imgs.button_press)
|
||||
.down_from(state.ids.zoom_lock_behavior_list, 12.0)
|
||||
.down_from(state.ids.aim_offset_y_slider, 12.0)
|
||||
.label(
|
||||
&self
|
||||
.localized_strings
|
||||
|
@ -33,7 +33,10 @@ use crate::{
|
||||
use client::Client;
|
||||
use common::{
|
||||
calendar::Calendar,
|
||||
comp::{self, ship::figuredata::VOXEL_COLLIDER_MANIFEST},
|
||||
comp::{
|
||||
self, item::ItemDesc, ship::figuredata::VOXEL_COLLIDER_MANIFEST, slot::EquipSlot,
|
||||
tool::ToolKind,
|
||||
},
|
||||
outcome::Outcome,
|
||||
resources::{DeltaTime, TimeScale},
|
||||
terrain::{BlockKind, TerrainChunk, TerrainGrid},
|
||||
@ -526,6 +529,7 @@ impl Scene {
|
||||
audio: &mut AudioFrontend,
|
||||
scene_data: &SceneData,
|
||||
client: &Client,
|
||||
settings: &Settings,
|
||||
) {
|
||||
span!(_guard, "maintain", "Scene::maintain");
|
||||
// Get player position.
|
||||
@ -621,6 +625,19 @@ impl Scene {
|
||||
.get(scene_data.viewpoint_entity)
|
||||
.map(|p| p.on_ground.is_some());
|
||||
|
||||
let player_entity = client.entity();
|
||||
let holding_ranged = client
|
||||
.inventories()
|
||||
.get(player_entity)
|
||||
.and_then(|inv| inv.equipped(EquipSlot::ActiveMainhand))
|
||||
.and_then(|item| item.tool_info())
|
||||
.is_some_and(|tool_kind| {
|
||||
matches!(
|
||||
tool_kind,
|
||||
ToolKind::Bow | ToolKind::Staff | ToolKind::Sceptre
|
||||
)
|
||||
});
|
||||
|
||||
let up = match self.camera.get_mode() {
|
||||
CameraMode::FirstPerson => {
|
||||
if viewpoint_rolling {
|
||||
@ -632,15 +649,29 @@ impl Scene {
|
||||
viewpoint_eye_height
|
||||
}
|
||||
},
|
||||
CameraMode::ThirdPerson if scene_data.is_aiming && holding_ranged => {
|
||||
viewpoint_height * 1.16 + settings.gameplay.aim_offset_y
|
||||
},
|
||||
CameraMode::ThirdPerson if scene_data.is_aiming => viewpoint_height * 1.16,
|
||||
CameraMode::ThirdPerson => viewpoint_eye_height,
|
||||
CameraMode::Freefly => 0.0,
|
||||
};
|
||||
|
||||
let right = match self.camera.get_mode() {
|
||||
CameraMode::FirstPerson => 0.0,
|
||||
CameraMode::ThirdPerson if scene_data.is_aiming && holding_ranged => {
|
||||
settings.gameplay.aim_offset_x
|
||||
},
|
||||
CameraMode::ThirdPerson => 0.0,
|
||||
CameraMode::Freefly => 0.0,
|
||||
};
|
||||
|
||||
// Alter camera position to match player.
|
||||
let tilt = self.camera.get_orientation().y;
|
||||
let dist = self.camera.get_distance();
|
||||
|
||||
Vec3::unit_z() * (up * viewpoint_scale - tilt.min(0.0).sin() * dist * 0.6)
|
||||
+ self.camera.right() * (right * viewpoint_scale)
|
||||
} else {
|
||||
self.figure_mgr
|
||||
.viewpoint_offset(scene_data, scene_data.viewpoint_entity)
|
||||
|
@ -50,6 +50,7 @@ use crate::{
|
||||
menu::char_selection::CharSelectionState,
|
||||
render::{Drawer, GlobalsBindGroup},
|
||||
scene::{camera, CameraMode, DebugShapeId, Scene, SceneData},
|
||||
session::target::ray_entities,
|
||||
settings::Settings,
|
||||
window::{AnalogGameInput, Event},
|
||||
Direction, GlobalState, PlayState, PlayStateResult,
|
||||
@ -1349,8 +1350,88 @@ impl PlayState for SessionState {
|
||||
if !self.free_look {
|
||||
self.walk_forward_dir = self.scene.camera().forward_xy();
|
||||
self.walk_right_dir = self.scene.camera().right_xy();
|
||||
self.inputs.look_dir =
|
||||
Dir::from_unnormalized(cam_dir + aim_dir_offset).unwrap();
|
||||
|
||||
let client = self.client.borrow();
|
||||
|
||||
let holding_ranged = client
|
||||
.inventories()
|
||||
.get(player_entity)
|
||||
.and_then(|inv| inv.equipped(EquipSlot::ActiveMainhand))
|
||||
.and_then(|item| item.tool_info())
|
||||
.is_some_and(|tool_kind| {
|
||||
matches!(
|
||||
tool_kind,
|
||||
ToolKind::Bow | ToolKind::Staff | ToolKind::Sceptre
|
||||
)
|
||||
});
|
||||
|
||||
let dir = if is_aiming
|
||||
&& holding_ranged
|
||||
&& self.scene.camera().get_mode() == CameraMode::ThirdPerson
|
||||
{
|
||||
// Shoot ray from camera focus forwards and get the point it hits an
|
||||
// entity or terrain. The ray starts from the camera focus point
|
||||
// so that the player won't aim at things behind them, in front of the
|
||||
// camera.
|
||||
let ray_start = self.scene.camera().get_focus_pos();
|
||||
let entity_ray_end = ray_start + cam_dir * 1000.0;
|
||||
let terrain_ray_end = ray_start + cam_dir * 1000.0;
|
||||
|
||||
let aim_point = {
|
||||
// Get the distance to nearest entity and terrain
|
||||
let entity_dist =
|
||||
ray_entities(&client, ray_start, entity_ray_end, 1000.0).0;
|
||||
let terrain_ray_distance = client
|
||||
.state()
|
||||
.terrain()
|
||||
.ray(ray_start, terrain_ray_end)
|
||||
.max_iter(1000)
|
||||
.until(Block::is_solid)
|
||||
.cast()
|
||||
.0;
|
||||
|
||||
// Return the hit point of whichever was smaller
|
||||
ray_start + cam_dir * entity_dist.min(terrain_ray_distance)
|
||||
};
|
||||
|
||||
// Get player orientation
|
||||
let ori = client
|
||||
.state()
|
||||
.read_storage::<comp::Ori>()
|
||||
.get(player_entity)
|
||||
.copied()
|
||||
.unwrap();
|
||||
// Get player scale
|
||||
let scale = client
|
||||
.state()
|
||||
.read_storage::<comp::Scale>()
|
||||
.get(player_entity)
|
||||
.copied()
|
||||
.unwrap_or(comp::Scale(1.0));
|
||||
// Get player body offsets
|
||||
let body = client
|
||||
.state()
|
||||
.read_storage::<comp::Body>()
|
||||
.get(player_entity)
|
||||
.copied()
|
||||
.unwrap();
|
||||
let body_offsets = body.projectile_offsets(ori.look_vec(), scale.0);
|
||||
|
||||
// Get direction from player character to aim point
|
||||
let player_pos = client
|
||||
.state()
|
||||
.read_storage::<Pos>()
|
||||
.get(player_entity)
|
||||
.copied()
|
||||
.unwrap();
|
||||
|
||||
drop(client);
|
||||
aim_point - (player_pos.0 + body_offsets)
|
||||
} else {
|
||||
cam_dir + aim_dir_offset
|
||||
};
|
||||
|
||||
self.inputs.look_dir = Dir::from_unnormalized(dir).unwrap();
|
||||
}
|
||||
}
|
||||
self.inputs.strafing = matches!(
|
||||
@ -2040,6 +2121,7 @@ impl PlayState for SessionState {
|
||||
&mut global_state.audio,
|
||||
&scene_data,
|
||||
&client,
|
||||
&global_state.settings,
|
||||
);
|
||||
|
||||
// Process outcomes from client
|
||||
|
@ -77,6 +77,9 @@ pub enum Gameplay {
|
||||
ChangeBowZoom(bool),
|
||||
ChangeZoomLock(bool),
|
||||
|
||||
AdjustAimOffsetX(f32),
|
||||
AdjustAimOffsetY(f32),
|
||||
|
||||
ResetGameplaySettings,
|
||||
}
|
||||
#[derive(Clone)]
|
||||
@ -427,6 +430,12 @@ impl SettingsChange {
|
||||
Gameplay::ChangeZoomLock(state) => {
|
||||
settings.gameplay.zoom_lock = state;
|
||||
},
|
||||
Gameplay::AdjustAimOffsetX(offset) => {
|
||||
settings.gameplay.aim_offset_x = offset;
|
||||
},
|
||||
Gameplay::AdjustAimOffsetY(offset) => {
|
||||
settings.gameplay.aim_offset_y = offset;
|
||||
},
|
||||
Gameplay::ResetGameplaySettings => {
|
||||
// Reset Gameplay Settings
|
||||
settings.gameplay = GameplaySettings::default();
|
||||
|
@ -13,6 +13,7 @@ use common::{
|
||||
vol::ReadVol,
|
||||
};
|
||||
use common_base::span;
|
||||
use common_systems::phys::closest_points_3d;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Target<T> {
|
||||
@ -238,3 +239,111 @@ pub(super) fn targets_under_cursor(
|
||||
terrain_target,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn ray_entities(
|
||||
client: &Client,
|
||||
start: Vec3<f32>,
|
||||
end: Vec3<f32>,
|
||||
cast_dist: f32,
|
||||
) -> (f32, Option<Entity>) {
|
||||
let player_entity = client.entity();
|
||||
let ecs = client.state().ecs();
|
||||
let positions = ecs.read_storage::<comp::Pos>();
|
||||
let colliders = ecs.read_storage::<comp::Collider>();
|
||||
|
||||
let mut nearby = (
|
||||
&ecs.entities(),
|
||||
&positions,
|
||||
&colliders,
|
||||
)
|
||||
.join()
|
||||
.filter(|(e, _, _)| *e != player_entity)
|
||||
.map(|(e, p, c)| {
|
||||
let height = c.get_height();
|
||||
let radius = c.bounding_radius().max(height / 2.0);
|
||||
// Move position up from the feet
|
||||
let pos = Vec3::new(p.0.x, p.0.y, p.0.z + c.get_z_limits(1.0).0 + height/2.0);
|
||||
// Distance squared from start to the entity
|
||||
let dist_sqr = pos.distance_squared(start);
|
||||
(e, pos, radius, dist_sqr, c)
|
||||
})
|
||||
// Roughly filter out entities farther than ray distance
|
||||
.filter(|(_, _, _, d_sqr, _)| *d_sqr <= cast_dist.powi(2))
|
||||
.collect::<Vec<_>>();
|
||||
// Sort by distance
|
||||
nearby.sort_unstable_by(|a, b| a.3.partial_cmp(&b.3).unwrap());
|
||||
|
||||
let seg_ray = LineSegment3 { start, end };
|
||||
|
||||
let entity = nearby.iter().find_map(|(e, p, r, _, c)| {
|
||||
let nearest = seg_ray.projected_point(*p);
|
||||
|
||||
return match c {
|
||||
comp::Collider::CapsulePrism {
|
||||
p0,
|
||||
p1,
|
||||
radius,
|
||||
z_min,
|
||||
z_max,
|
||||
} => {
|
||||
// Check if the nearest point is within the capsule's inclusive radius (radius
|
||||
// from center to furthest possible edge corner) If not, then
|
||||
// the ray doesn't intersect the capsule at all and we can skip it
|
||||
if nearest.distance_squared(*p) > (r * 3.0_f32.sqrt()).powi(2) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let entity_rotation = ecs
|
||||
.read_storage::<comp::Ori>()
|
||||
.get(*e)
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
let entity_position = ecs.read_storage::<comp::Pos>().get(*e).copied().unwrap();
|
||||
let world_p0 = entity_position.0
|
||||
+ (entity_rotation.to_quat()
|
||||
* Vec3::new(p0.x, p0.y, z_min + c.get_height() / 2.0));
|
||||
let world_p1 = entity_position.0
|
||||
+ (entity_rotation.to_quat()
|
||||
* Vec3::new(p1.x, p1.y, z_min + c.get_height() / 2.0));
|
||||
|
||||
// Get the closest points between the ray and the capsule's line segment
|
||||
// If the capsule's line segment is a point, then the closest point is the point
|
||||
// itself
|
||||
let (p_a, p_b) = if p0 != p1 {
|
||||
let seg_capsule = LineSegment3 {
|
||||
start: world_p0,
|
||||
end: world_p1,
|
||||
};
|
||||
closest_points_3d(seg_ray, seg_capsule)
|
||||
} else {
|
||||
let nearest = seg_ray.projected_point(world_p0);
|
||||
(nearest, world_p0)
|
||||
};
|
||||
|
||||
// Check if the distance between the closest points are within the capsule
|
||||
// prism's radius on the xy plane and if the closest points are
|
||||
// within the capsule prism's z range
|
||||
let distance = p_a.xy().distance_squared(p_b.xy());
|
||||
if distance < radius.powi(2)
|
||||
&& p_a.z >= entity_position.0.z + z_min
|
||||
&& p_a.z <= entity_position.0.z + z_max
|
||||
{
|
||||
return Some((p_a.distance(start), Entity(*e)));
|
||||
}
|
||||
|
||||
// If all else fails, then the ray doesn't intersect the capsule
|
||||
None
|
||||
},
|
||||
// TODO: handle other collider types, for now just use the bounding sphere
|
||||
_ => {
|
||||
if nearest.distance_squared(*p) < r.powi(2) {
|
||||
return Some((nearest.distance(start), Entity(*e)));
|
||||
}
|
||||
None
|
||||
},
|
||||
};
|
||||
});
|
||||
entity
|
||||
.map(|(dist, e)| (dist, Some(e)))
|
||||
.unwrap_or((cast_dist, None))
|
||||
}
|
||||
|
@ -21,6 +21,8 @@ pub struct GameplaySettings {
|
||||
pub auto_camera: bool,
|
||||
pub bow_zoom: bool,
|
||||
pub zoom_lock: bool,
|
||||
pub aim_offset_x: f32,
|
||||
pub aim_offset_y: f32,
|
||||
}
|
||||
|
||||
impl Default for GameplaySettings {
|
||||
@ -42,6 +44,8 @@ impl Default for GameplaySettings {
|
||||
auto_camera: false,
|
||||
bow_zoom: true,
|
||||
zoom_lock: false,
|
||||
aim_offset_x: 1.0,
|
||||
aim_offset_y: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user