How can I move and rotate an isometric camera in 3D?

Godot Version

4.2.1

Question

Hello!

I’m new in Godot and game development in general and I’ve been having some troubles setting up a camera.

Basically I want to create an isometric camera that allows movement (up, down, left, right) and rotation every 90 degrees.

I have a Camera3D node set as orthographic that is rotated -30 on X and 45 in Y. I’m using it as a child of a node3D because I want it to rotate around the center of what the camera is looking at.

I’m moving the camera by changing the position of the Node3D in the z and x axis, but when I rotate it (90 degrees in Y) every input changes, which makes sense. If I rotate 180 degrees now every input is inverted, up is down, left is right, etc.

I don’t know how to move the camera so that ‘left’ is always ‘left’ independently of how the camera is rotated. How could I achieve this?

Sorry for the noob question! Thanks!

2 Likes

You lost me at “rotation every 90 degrees”

I mean rotating the camera in 90 degrees intervals.

That’s a Unity tutorial that does something similar…

Sorry if I didn’t explain it correctly, English is not my first language.

I do something like this:

if Input.is_action_pressed("camera_move_left"):
	translate(-transform.basis.x * _CAMERA_MOVE_SPEED * delta)
elif Input.is_action_pressed("camera_move_right"):
	translate(transform.basis.x * _CAMERA_MOVE_SPEED * delta)
if Input.is_action_pressed("camera_move_forward"):
	translate(-transform.basis.z * _CAMERA_MOVE_SPEED * delta)
elif Input.is_action_pressed("camera_move_backward"):
	translate(transform.basis.z * _CAMERA_MOVE_SPEED * delta)

Basically you have move the camera along it’s forward and sideways vectors (transform.basis.z and x)

1 Like

I don’t want the camera to move “forward” and “back”, that would be pretty much zooming.

I want it to go up and down. Something like the Age of Empires 2 camera. The difference is that I want to be able to rotate it, for example, to see a unit from 4 different angles (always isometric), while keeping the correct inputs: ‘move_camera_right’ is always right, regardless of how the camera is rotated at that moment.

I tried with the code you shared and I changed the ‘z’ for ‘y’ and it worked for the up and down movement. The ‘right’ and ‘left’ input changes to be ‘forward’ and ‘backwards’ when the the parent node is rotated 90 or 270 degrees, and it becomes inverted at 180 (left is right).

Is there any solution to that? I don’t understand why the vectors of the camera would change because of the rotation of the parent node.

If I change the Y rotation of the Camera3D node the vectors don’t change and it works, the problem is that it’s offcenter. The camera should rotate around the parent node to always be focusing the center. (If I’m looking at a unit in the center of the screen, it should look like it’s rotating in 90 degrees steps, but staying in the center of the screen)

Oh yeah, I understand, I “simplified” my code and didn’t realize that it won’t work properly, sorry. Anyway here’s my actual code. (I think) it moves the way you want it + it can “look up/down” + set camera height with scrolling.
The node hierarchy look like:

  • _free_camera (movement)
    • _rotating_hinge (rotates on y axis)
      • _free_cam_mount (rotates on x axis to look up / down + height)
        • Camera3D
          The camera is set to “top_level” and I move and rotate it with lerp() at the bottom of the script. I know it’s not exactly what you want but hope it helps.
func _input(event):

	if state == Camera.State.FREE_CAMERA:
		if event is InputEventMouseMotion:
			if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
				_free_cam_mount.rotation.x -= event.relative.y * _SENSITIVITY_HORIZONTAL
				_rotating_hinge.rotation.y -= event.relative.x * _SENSITIVITY_VERTICAL
				_free_cam_mount.rotation.x = clamp(_free_cam_mount.rotation.x, _CAMERA_PITCH_DOWN_LIMIT, _CAMERA_PITCH_UP_LIMIT)
				get_viewport().set_input_as_handled()
			return

		if event is InputEventMouseButton:
			match event.button_index:
				MOUSE_BUTTON_MIDDLE: # Only allows rotation if middle button down
					if event.pressed:
						Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
					else:
						Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
				MOUSE_BUTTON_WHEEL_UP:
					if _not_hitting_ground():
						_free_cam_mount.position -= _free_cam_mount.transform.basis.z * _CAMERA_ZOOM_SPEED
				MOUSE_BUTTON_WHEEL_DOWN:
					_free_cam_mount.position += _free_cam_mount.transform.basis.z * _CAMERA_ZOOM_SPEED
				_:
					return	# if no match -> return so event doesn't get set as handled
			get_viewport().set_input_as_handled()

func _process(delta):
	match state:
		Camera.State.FREE_CAMERA:
			if Input.is_action_pressed("camera_move_left"):
				_free_camera.translate(-_rotating_hinge.transform.basis.x * _CAMERA_MOVE_SPEED * delta)
			elif Input.is_action_pressed("camera_move_right"):
				_free_camera.translate(_rotating_hinge.transform.basis.x * _CAMERA_MOVE_SPEED * delta)
			if Input.is_action_pressed("camera_move_forward"):
				_free_camera.translate(-_rotating_hinge.transform.basis.z * _CAMERA_MOVE_SPEED * delta)
			elif Input.is_action_pressed("camera_move_backward"):
				_free_camera.translate(_rotating_hinge.transform.basis.z * _CAMERA_MOVE_SPEED * delta)

			if not global_position.is_equal_approx(_free_cam_mount.global_position):
				global_position = lerp(global_position, _free_cam_mount.global_position, _MOVE_LERP_SPEED * delta)
			if not is_equal_approx(global_rotation.x, _free_cam_mount.global_rotation.x):
				global_rotation.x = lerp_angle(global_rotation.x, _free_cam_mount.global_rotation.x, _ROTATE_LERP_SPEED * delta)
			if not is_equal_approx(global_rotation.y, _free_cam_mount.global_rotation.y):
				global_rotation.y = lerp_angle(global_rotation.y, _free_cam_mount.global_rotation.y, _ROTATE_LERP_SPEED * delta)

Since the Camera is already child of Node3D it will follow it. So just rotate the camera.

If I rotate the camera everything will be offset, I want the camera to be always looking at the same point regardless of the rotation, I can get this effect by rotating the world instead but I wanted to avoid that.

I made a short video explaining the problem:

There is too few details for me to understand. Project structure and your implementation would be helpful.

tree

It’s just a simple scene to test it, and this is the code I’m using right now.

extends Node3D

@onready var camera = $Camera3D

var camera_move_speed = 15

func _process(delta):
	
	if Input.is_action_pressed("camera_move_left"):
		camera.translate(-transform.basis.x * camera_move_speed * delta)
	elif Input.is_action_pressed("camera_move_right"):
		camera.translate(transform.basis.x * camera_move_speed * delta)
		
	if Input.is_action_pressed("camera_move_up"):
		camera.translate(transform.basis.y * camera_move_speed * delta)
	elif Input.is_action_pressed("camera_move_down"):
		camera.translate(-transform.basis.y * camera_move_speed * delta)

	if Input.is_action_just_pressed("camera_rotate_left"):
		rotation_degrees.y += 90

	if Input.is_action_just_pressed("camera_rotate_right"):
		rotation_degrees.y -= 90

Hello

In case you haven’t solved your problem ill share what Ive done.

Biggest problem I can see is that you’re moving the camera along the Y axis when you want it to be along the Z axis. Moving along the Y axis will move it up and down when you want it to pan along the X and Z axis. If the camera is close enough to your environment and you move it down the Y axis, it could clip into which is probably what’s happening when you get your clipping.

Next, you don’t want to move the camera directly but its parent node. You can think of this node as the pivot point.

My file structure is pretty much the same as yours.

main
   Water
   GridMap
   CameraPivot (Node3D)
      MeshInstance3D
      Camera3D

The CameraPivot is visualised by a MeshInstance and the Camera is translated pretty far so there’s no risk of clipping and also rotated down -45 degrees to look at the environment
image

Instead of moving the camera directly, I move and rotate the CameraPivot…

extends Node3D

var camera_move_speed = 0.5

func _process(delta):
	
	var forward = transform.basis.z.normalized() * camera_move_speed
	
	if Input.is_action_pressed("left"):
		transform.origin += forward.cross(Vector3.UP)
	if Input.is_action_pressed("right"):
		transform.origin -= forward.cross(Vector3.UP)
	if Input.is_action_pressed("up"):
		transform.origin -= forward
	if Input.is_action_pressed("down"):
		transform.origin += forward

	if Input.is_action_just_pressed("rotate_left"):
		rotation_degrees.y -= 45
	if Input.is_action_just_pressed("rotate_right"):
		rotation_degrees.y += 45