FPS camera - Quaternions - movement slows down when looking up and down

Godot version 4.3

Hi,

I’m currently teaching myself Quaternions and to better grasp the concept I’ve decided to make a FPS camera ( camera orientation with the mouse and movement with WASD).

So far, everything works with a single script using GDscript, walking follows camera orientation as intended.

The only problem I’m facing so far is that when I look up or down the movement slows down gradually and stops when watching the ground or the sky.

Here’s the code :

extends Camera3D

@export_range(0,100) var look_speed : float = 50
@export_range(0,100) var look_smooth : float = 50
@export_range(0,100) var move_speed : float = 50

var pos
var move_scale = 0.1
var mouse_locked = true
var look_scale = 0.00001
var threshold = 0.00001
var target_basis = Basis.IDENTITY
var smooth_scale = 0.001

# Called when the node enters the scene tree for the first time.
func _ready() -> void:
	pos = get_node("/root/Test2/Player/Camera_dummy") # Here for initial camera placement
	self.position = pos.global_position
	Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
	var move_dir = check_buttons()
	transform.basis = transform.basis.slerp(target_basis, look_smooth * smooth_scale)
	move_dir = transform.basis * move_dir
	var next_pos = self.global_position + (move_dir * move_speed * move_scale * delta)
	self.global_position = Vector3(next_pos.x, self.global_position.y, next_pos.z)
	
func _input(event: InputEvent) -> void:
	if event is InputEventMouseMotion and mouse_locked:
		var mouse_delta = -event.relative * look_speed * look_scale
		var delta_y_max = (-target_basis.z).angle_to(Vector3.UP * sign(mouse_delta.y))
		mouse_delta.y = clamp(abs(mouse_delta.y), 0.0, delta_y_max - threshold) * sign(mouse_delta.y)
		var horz_quat = Quaternion(Vector3.UP * target_basis, mouse_delta.x)
		var vert_quat = Quaternion(Vector3.RIGHT, mouse_delta.y)
		target_basis *= Basis(horz_quat * vert_quat)
		target_basis = target_basis.orthonormalized()

func check_buttons():
	var dir = Vector3.ZERO
	if Input.is_action_pressed("forward"):
		dir += Vector3.FORWARD
	if Input.is_action_pressed("back"):
		dir += Vector3.BACK
	if Input.is_action_pressed("left"):
		dir += Vector3.LEFT
	if Input.is_action_pressed("right"):
		dir += Vector3.RIGHT
	if not dir.is_equal_approx(Vector3.ZERO):
		dir = dir.normalized()
	return dir

So far, nothing surprising since my transform.basis has been rotated and the Z axis is not aligned with the world anymore hence the length of Z in my “move_dir” and the speed diminution.

What I’ve tried :

  • Making the camera a children of a CharacterBody3D and applying camera rotation on the Y axis to it. It works but cornering becomes choppy since, I suppose, CharacterBody and Camera rotation are fighting each other.

  • Normalizing “move_dir” after multiplying it with transform.basis. No luck. Printing the value in the console shows values different from 0 and 1. I think it’s because the initial values are decimal below 1.

  • Trying to force normalize on “move_dir” with a function that override the values if it’s below or over 0. Funky movement everytime !

I think I’m missing a good chunk of knowledge on how to handle transform properly so any help would be much appreciated.

Thanks a bunch.

Seems like your best bet if you are looking for strictly quaternion rotations.

You need to rotate move_dir only around the Y axis to preserve planar movement.

Will try that. Many thanks.

Thank you for pointing me in the right direction !

I am creating a third Quaternion from Vector3.UP and mouse movement on x only and updating a second basis with this Quaternion. In the end, I’ve got two Transform one for the Camera Orientation and one for the movement.

Thank you again, learned a lot.

The code if someone needs it :

extends Camera3D

@export_range(0,100) var look_speed : float = 50
@export_range(0,100) var look_smooth : float = 50
@export_range(0,100) var move_speed : float = 50

var pos
var move_scale = 0.1
var mouse_locked = true
var look_scale = 0.00001
var threshold = 0.00001
var target_basis = Basis.IDENTITY
var second_basis = Basis.IDENTITY
var smooth_scale = 0.001

# Called when the node enters the scene tree for the first time.
func _ready() -> void:
	pos = get_node("/root/Test2/Player/Camera_dummy") # Here for initial camera placement
	self.position = pos.global_position
	Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
	var move_dir = check_buttons()
	transform.basis = transform.basis.slerp(target_basis, look_smooth * smooth_scale)
	move_dir = second_basis * move_dir
	var next_pos = self.global_position + (move_dir * move_speed * move_scale * delta)
	self.global_position = Vector3(next_pos.x, self.global_position.y, next_pos.z)

func _input(event: InputEvent) -> void:
	if event is InputEventMouseMotion and mouse_locked:
		var mouse_delta = -event.relative * look_speed * look_scale
		var delta_y_max = (-target_basis.z).angle_to(Vector3.UP * sign(mouse_delta.y))
		mouse_delta.y = clamp(abs(mouse_delta.y), 0.0, delta_y_max - threshold) * sign(mouse_delta.y)
		var horz_quat = Quaternion(Vector3.UP * target_basis, mouse_delta.x)
		var horz_quat_no_pitch = Quaternion(Vector3.UP * second_basis, mouse_delta.x)
		var vert_quat = Quaternion(Vector3.RIGHT, mouse_delta.y)
		target_basis *= Basis(horz_quat * vert_quat)
		second_basis *= Basis(horz_quat_no_pitch)
		target_basis = target_basis.orthonormalized()

func check_buttons():
	var dir = Vector3.ZERO
	if Input.is_action_pressed("forward"):
		dir += Vector3.FORWARD
	if Input.is_action_pressed("back"):
		dir += Vector3.BACK
	if Input.is_action_pressed("left"):
		dir += Vector3.LEFT
	if Input.is_action_pressed("right"):
		dir += Vector3.RIGHT
	if not dir.is_equal_approx(Vector3.ZERO):
		dir = dir.normalized()
	return dir
1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.