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:
Illia Denysenko 2024-02-02 16:59:13 +00:00
commit 2c9ceb51d5
8 changed files with 351 additions and 5 deletions

View File

@ -57,6 +57,8 @@ hud-settings-walking_speed_behavior = Walking speed behavior
hud-settings-walking_speed = Walking speed hud-settings-walking_speed = Walking speed
hud-settings-camera_clamp_behavior = Camera clamp behavior hud-settings-camera_clamp_behavior = Camera clamp behavior
hud-settings-zoom_lock_behavior = Camera zoom lock 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-player_physics_behavior = Player physics (experimental)
hud-settings-stop_auto_walk_on_input = Stop auto walk on movement hud-settings-stop_auto_walk_on_input = Stop auto walk on movement
hud-settings-auto_camera = Auto camera hud-settings-auto_camera = Auto camera

View File

@ -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 /// Find pushback vector and collision_distance we assume between this
/// colliders assuming that only one of them is capsule prism. /// colliders assuming that only one of them is capsule prism.
fn capsule2cylinder(c0: ColliderContext, c1: ColliderContext) -> (Vec2<f32>, f32) { fn capsule2cylinder(c0: ColliderContext, c1: ColliderContext) -> (Vec2<f32>, f32) {

View File

@ -59,6 +59,12 @@ widget_ids! {
bow_zoom_label, bow_zoom_label,
zoom_lock_button, zoom_lock_button,
zoom_lock_label, 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) .color(TEXT_COLOR)
.set(state.ids.zoom_lock_label, ui); .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 // Reset the gameplay settings to the default settings
if Button::image(self.imgs.button) if Button::image(self.imgs.button)
.w_h(RESET_BUTTONS_WIDTH, RESET_BUTTONS_HEIGHT) .w_h(RESET_BUTTONS_WIDTH, RESET_BUTTONS_HEIGHT)
.hover_image(self.imgs.button_hover) .hover_image(self.imgs.button_hover)
.press_image(self.imgs.button_press) .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( .label(
&self &self
.localized_strings .localized_strings

View File

@ -33,7 +33,10 @@ use crate::{
use client::Client; use client::Client;
use common::{ use common::{
calendar::Calendar, calendar::Calendar,
comp::{self, ship::figuredata::VOXEL_COLLIDER_MANIFEST}, comp::{
self, item::ItemDesc, ship::figuredata::VOXEL_COLLIDER_MANIFEST, slot::EquipSlot,
tool::ToolKind,
},
outcome::Outcome, outcome::Outcome,
resources::{DeltaTime, TimeScale}, resources::{DeltaTime, TimeScale},
terrain::{BlockKind, TerrainChunk, TerrainGrid}, terrain::{BlockKind, TerrainChunk, TerrainGrid},
@ -526,6 +529,7 @@ impl Scene {
audio: &mut AudioFrontend, audio: &mut AudioFrontend,
scene_data: &SceneData, scene_data: &SceneData,
client: &Client, client: &Client,
settings: &Settings,
) { ) {
span!(_guard, "maintain", "Scene::maintain"); span!(_guard, "maintain", "Scene::maintain");
// Get player position. // Get player position.
@ -621,6 +625,19 @@ impl Scene {
.get(scene_data.viewpoint_entity) .get(scene_data.viewpoint_entity)
.map(|p| p.on_ground.is_some()); .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() { let up = match self.camera.get_mode() {
CameraMode::FirstPerson => { CameraMode::FirstPerson => {
if viewpoint_rolling { if viewpoint_rolling {
@ -632,15 +649,29 @@ impl Scene {
viewpoint_eye_height 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 if scene_data.is_aiming => viewpoint_height * 1.16,
CameraMode::ThirdPerson => viewpoint_eye_height, CameraMode::ThirdPerson => viewpoint_eye_height,
CameraMode::Freefly => 0.0, 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. // Alter camera position to match player.
let tilt = self.camera.get_orientation().y; let tilt = self.camera.get_orientation().y;
let dist = self.camera.get_distance(); let dist = self.camera.get_distance();
Vec3::unit_z() * (up * viewpoint_scale - tilt.min(0.0).sin() * dist * 0.6) Vec3::unit_z() * (up * viewpoint_scale - tilt.min(0.0).sin() * dist * 0.6)
+ self.camera.right() * (right * viewpoint_scale)
} else { } else {
self.figure_mgr self.figure_mgr
.viewpoint_offset(scene_data, scene_data.viewpoint_entity) .viewpoint_offset(scene_data, scene_data.viewpoint_entity)

View File

@ -50,6 +50,7 @@ use crate::{
menu::char_selection::CharSelectionState, menu::char_selection::CharSelectionState,
render::{Drawer, GlobalsBindGroup}, render::{Drawer, GlobalsBindGroup},
scene::{camera, CameraMode, DebugShapeId, Scene, SceneData}, scene::{camera, CameraMode, DebugShapeId, Scene, SceneData},
session::target::ray_entities,
settings::Settings, settings::Settings,
window::{AnalogGameInput, Event}, window::{AnalogGameInput, Event},
Direction, GlobalState, PlayState, PlayStateResult, Direction, GlobalState, PlayState, PlayStateResult,
@ -1349,8 +1350,88 @@ impl PlayState for SessionState {
if !self.free_look { if !self.free_look {
self.walk_forward_dir = self.scene.camera().forward_xy(); self.walk_forward_dir = self.scene.camera().forward_xy();
self.walk_right_dir = self.scene.camera().right_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!( self.inputs.strafing = matches!(
@ -2040,6 +2121,7 @@ impl PlayState for SessionState {
&mut global_state.audio, &mut global_state.audio,
&scene_data, &scene_data,
&client, &client,
&global_state.settings,
); );
// Process outcomes from client // Process outcomes from client

View File

@ -77,6 +77,9 @@ pub enum Gameplay {
ChangeBowZoom(bool), ChangeBowZoom(bool),
ChangeZoomLock(bool), ChangeZoomLock(bool),
AdjustAimOffsetX(f32),
AdjustAimOffsetY(f32),
ResetGameplaySettings, ResetGameplaySettings,
} }
#[derive(Clone)] #[derive(Clone)]
@ -427,6 +430,12 @@ impl SettingsChange {
Gameplay::ChangeZoomLock(state) => { Gameplay::ChangeZoomLock(state) => {
settings.gameplay.zoom_lock = 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 => { Gameplay::ResetGameplaySettings => {
// Reset Gameplay Settings // Reset Gameplay Settings
settings.gameplay = GameplaySettings::default(); settings.gameplay = GameplaySettings::default();

View File

@ -13,6 +13,7 @@ use common::{
vol::ReadVol, vol::ReadVol,
}; };
use common_base::span; use common_base::span;
use common_systems::phys::closest_points_3d;
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub struct Target<T> { pub struct Target<T> {
@ -238,3 +239,111 @@ pub(super) fn targets_under_cursor(
terrain_target, 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))
}

View File

@ -21,6 +21,8 @@ pub struct GameplaySettings {
pub auto_camera: bool, pub auto_camera: bool,
pub bow_zoom: bool, pub bow_zoom: bool,
pub zoom_lock: bool, pub zoom_lock: bool,
pub aim_offset_x: f32,
pub aim_offset_y: f32,
} }
impl Default for GameplaySettings { impl Default for GameplaySettings {
@ -42,6 +44,8 @@ impl Default for GameplaySettings {
auto_camera: false, auto_camera: false,
bow_zoom: true, bow_zoom: true,
zoom_lock: false, zoom_lock: false,
aim_offset_x: 1.0,
aim_offset_y: 0.0,
} }
} }
} }