Trouble with clamping 3d camera on y axis

Godot Version

4.2.2

Question

Hey I am having a bit of trouble with my camera script. I am clamping both the y and x axis but the y axis is giving me some trouble. Instead of clamping and looking at the desired direction it turns 180 degrees then clamps so basically looking backwards

Here is the code

if event is InputEventMouseMotion && camMove:
		
		cam.rotate_x(deg_to_rad(-event.relative.y * mouseSens))
		camBall.global_rotate(Vector3.UP, (deg_to_rad(-event.relative.x * mouseSens)))
		
		cam.rotation.x = clamp(cam.rotation.x, deg_to_rad(-30.0), deg_to_rad(30.0))
		camBall.rotation.y = clamp(camBall.rotation.y, deg_to_rad(-60.0), deg_to_rad(60.0))

I am new to gdscript and very bad at coding in general :slight_smile:

Is camBall’s y rotation 0 in the editor? Sounds like it might be facing the wrong direction by default.

For example if it’s y rotation is 180 in the editor then you have to calculate the clamp values from that, so it would be 120 - 240 (60 degrees to each direction from 180).

Yeah the rotation is 0.

Sorry for the late reply been really busy

What’s the reasoning for separating the camera’s rotation into two nodes (cam and camBall)? Perhaps you could explain your node setup for your camera so the context is more clear. We don’t know the hierarchy of these two nodes.

My guess is that your setup is causing your issue.

Oh yeah sorry about that, I am very new at this stuff.

So I have a 3d node on the player characters location with another 3d node as it’s child to allow for camera rotation around the player then I have another 3d node child to that child which is away from the player that handles the distance of the camera from the player then that one has a 3d node child which is the camBall then childed to that one is the cam, both camBall and cam has code to allow for looking around when holding middle mouse button and moving the mouse. So the hierarchy goes camBody → camBodyBall → camDistance → camBall → cam.

I am sure there is a way easier method on how to do this but as a complete noob this is the only thing that has worked the intended way for me

I see. Ideally, you should only have the camera on the player - none of these in-between parts. Camera motion should then be computed in a script (with vectors etc.).

Taking that aside, you’re saying that you have code on both cam and camBall which also rotate the camera? In summary, you have 3 scripts that rotate your node(s)?

Yes, I’ll paste all the parts of my code with everything but the camera controls removed

extends CharacterBody3D

var playerPos = $".".position
const mouseSens = 0.4
@onready var camBody = $CameraBody
@onready var camBodyBall = $CameraBody/CameraBodyBall
@onready var cam_distance = $CameraBody/CameraBodyBall/CamDistance
@onready var camBall = $CameraBody/CameraBodyBall/CamDistance/CameraBall
@onready var cam = $CameraBody/CameraBodyBall/CamDistance/CameraBall/PlayerCam
@onready var navigation_agent: NavigationAgent3D = $NavigationAgent3D
@onready var timer = $Timer

var camMinDist = 1
var camMaxDist = 20

var camMove = false
var lookatPlayer = true


func _input(event: InputEvent) -> void:
	# controls camera movement when middle mouse is held
	if event is InputEventMouseMotion && camMove:
		cam.rotate_x(deg_to_rad(-event.relative.y * mouseSens))
		camBall.global_rotate(Vector3.UP, (deg_to_rad(-event.relative.x * mouseSens)))


func _physics_process(delta: float) -> void:
	CameraControl(delta)

	# clamps camera position
	camBodyBall.rotation.x = clamp(camBodyBall.rotation.x, deg_to_rad(10.0), deg_to_rad(70.0))
	cam.rotation.x = clamp(cam.rotation.x, deg_to_rad(-30.0), deg_to_rad(30.0))
	cam_distance.position.z = clamp(cam_distance.position.z, -20, -3)
	camBall.rotation.y = clamp(camBall.rotation.y, deg_to_rad(-60.0), deg_to_rad(60.0))
	velocity = direction * movement_speed
	move_and_slide()
	

func CameraControl(delta):
	# Looks at player when middle mouse is not pressed
	if !Input.is_action_pressed("middle_button"):
		camMove = false
		Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
		if lookatPlayer:
			camBall.rotation = camBall.rotation.lerp(playerPos, delta * 6)
			cam.rotation = cam.rotation.lerp(playerPos, delta * 6)
		
	# disables auto look at player when pressing middle mouse to allow for camera movement
	elif Input.is_action_pressed("middle_button"):
		camMove = true
		lookatPlayer = false
		timer.stop()
		Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
	# starts timer to look back at player after middle mouse is released
	if Input.is_action_just_released("middle_button"):
		CamWait()
		print("timer started")
	#camBodyBall.rotation.x = clamp(camBodyBall.rotation.x, deg_to_rad(-30.0), deg_to_rad(30.0))
	
	# camera rotation controls
	if Input.is_action_pressed("kb_a") && lookatPlayer:
		camBody.rotate_y(-0.05)
	if Input.is_action_pressed("kb_d") && lookatPlayer:
		camBody.rotate_y(0.05)
	if Input.is_action_pressed("kb_w") && lookatPlayer:
		camBodyBall.rotate_x(0.05)
	if Input.is_action_pressed("kb_s") && lookatPlayer:
		camBodyBall.rotate_x(-0.05)
	
	# camera zoom controls
	if Input.is_action_just_pressed("mouse_wheel_up") && lookatPlayer:
		#camBall.position -= (camBall.position -playerPos).normalized()
		cam_distance.position.z += 1
	elif Input.is_action_just_pressed("mouse_wheel_down") && lookatPlayer:
		#camBall.position += (camBall.position -playerPos).normalized()
		cam_distance.position.z -= 1

# timer to resume camera player follow
func CamWait():
	timer.start()

# after timer ends start camera player follow
func _on_timer_timeout():
	lookatPlayer = true
	print("looking at player")

this is probably a lot just for a camera script

Your entire setup, including your script, is a lot to take in. Your code is decent from an organization standpoint but it lacks finesse. It’s not that your script is “a lot just for a camera script”, but it’s a lot compared to what it achieves.

When I look at it I am wondering - what is this?

        if lookatPlayer:
			camBall.rotation = camBall.rotation.lerp(playerPos, delta * 6)
			cam.rotation = cam.rotation.lerp(playerPos, delta * 6)

Surely this does not work, right? Vector3’s lerp() interpolates from one vector to another. It doesn’t make a rotation, represented in euler angles, look at a position.

You’re also switching between using local and global coordinates. Do you have any reason to do so?

I’m tempted to suggest that you get rid of all the camera nodes that are not actual cameras. It’s confusing to look at their similar names and their relation to one another makes any accompanying code extremely prone to band-aid solutions. Your approach to a camera system makes me frustrated.

I’m not sure I can say anything that can help you improve your current system. I think it would help everyone, including yourself, if you could describe what you want out of your camera system. Perhaps you could also explain what other things you have tried since:

That is so the camera pans back to the player character.

Here is a video on how it works.

This is how I want the camera system to work, just need to clamp when the camera looks away from the player which I have managed on the x rotation but on the y rotation it janks out

The video doesn’t load for me.

If I was you, I would try to locate the point in your code that causes your issue:

  1. Print out the value for the camera’s y-rotation before and after the clamp().
  2. Print out the y-rotation for the other “cam” objects (camBodyBall etc.).
  3. Locate the problem based off of that information.
  4. Implement a fix.