Sprite3D rotation relative to the camera

Godot Version

Godot 4.1.1

Question

I need help figuring out a solution to the following problem:

The Setup:

I have a Character that is a CharacterBody3D with a Sprite3D node attached to it, as well as an AnimationPlayer node.

The AnimationPlayer node has animations for 8 directions - (sprite facing forward, facing backwards, facing left, facing forward-left, etc.)

In the Level scene, I have a camera that attaches itself to the character and can rotate around it along Y axis.

I am using the camera.global_transform.basis for calculating the forward vector of movement, so that when the player presses “move_up”, the character moves in the same direction the camera is looking.

Inside the Character script, I also have a a couple of functions that update the character sprite’s animation based on movement direction relative to camera, so that when the player moves, for example, to the left of where the camera is looking, the AnimationPlayer plays frames associated with the Sprite3D looking to the right. When the character moves towards the camera, the Sprite looks into the camera, etc.

And there’s also another function that handles directional animations of Sprite3D based on where the camera is looking. It uses the dot product between the character’s basis and camera basis to update the animation so when, for example, the camera rotates to the left, the sprite, instead of rotating itself with the camera, updates its animation with different directional animation to simulate the change of perspective.

The Problem:

The last two functions work separately from each other. When the character moves in any direction, the animation updates according to the direction, making the sprite show its back to the camera no matter what direction the camera is facing, but should the character stop, it will return to animation that is based on the dot_product calculation.

For the past couple of days, I have been trying to figure out how to make the animation update for both of these cases, so that when the player stops moving, the sprite’s facing direction remains unchanged and the camera rotation updates the directional animation based on the sprite’s front that is relative to the camera.
I’ve tried a bunch of different methods, but most of them, were, admittedly, shots in the dark that have failed.

I realize my explanation of the problem might be a bit all over the place, so here’s a video to demonstrate the problem to make it clearer:

And here’s a code of the function that is responsible for calculating the camera’s spatial relation to the sprite:

func update_anim_to_camera():
func update_anim_to_camera():
	var camera_fwd = main_camera.global_transform.basis.z
	print("camera_fwd = ", camera_fwd)
	var char_forward = global_transform.basis.z #Vector3(input_dir.x,0,1)
	print("char_fwd = ", char_forward)
	var char_left = global_transform.basis.x #Vector3(0,0,input_dir.y)
	print("char_left = ", char_left)
	
	var l_dot = char_left.dot(camera_fwd)
	print("l_dot = ", l_dot)
	var f_dot = char_forward.dot(camera_fwd)
	print("f_dot = ", f_dot)
	
	if f_dot < -0.65:
		sprite_facing = "front"
	elif f_dot > 0.65:
		sprite_facing = "back"
	else:
		if abs(f_dot) < 0.3 and l_dot > 0:
			sprite_facing = "right" # left sprite
		elif abs(f_dot) < 0.3 and l_dot < 0:
			sprite_facing = "left" # left sprite
		elif f_dot < 0 and l_dot > 0:
			sprite_facing = "frontright" # forward left sprite
		elif f_dot < 0 and l_dot < 0:
			sprite_facing = "frontleft" # forward left sprite
		else:
			if l_dot < 0:
				sprite_facing = "backleft" # back left sprite
			else:
				sprite_facing = "backright"

Everything else in the code works as intended… I think. The problem is specifically with this function. I pretty much copy-pasted it from a youtube tutorial and I admit I don’t fully understand the calculations happening here, so I am not sure what I need to do and how to do it to achieve the desired behavior.

But just in case, here’s the full code I have inside the CharacterBody3D for context:

CharacterBody3D Code
extends CharacterBody3D

var main_camera = null
var front = Vector3 (0,0,1)

@export var move_speed := 200.0
var input_dir = Vector2(0,0)

@onready var anim_player = $AnimationPlayer

var sprite_facing := "back"
var anim_rows : Dictionary = {
	"right":0,
	"backright":1,
	"back":2,
	"backleft":3,
	"left":4,
	"frontleft":5,
	"front":6,
	"frontright":7
}

func _ready():
	anim_player.play("idle")


func set_camera(camera):
	main_camera = camera


func _physics_process(delta):
	velocity.x = get_movement_direction().x * move_speed * delta
	velocity.z = get_movement_direction().z * move_speed * delta
	move_and_slide()
	
	if get_movement_direction():
		anim_player.play("run")
		update_front_direction(input_dir)
	else:
		anim_player.play('idle')
	
	update_anim_to_camera()
	update_sprite_facing()
	
	
	animation_change()


func get_movement_direction():
	input_dir.x = Input.get_action_strength("move_right") - Input.get_action_strength("move_left")
	input_dir.y = Input.get_action_strength("move_down") - Input.get_action_strength("move_up")
	var direction = (main_camera.transform.basis * Vector3 (input_dir.x, 0, input_dir.y)).normalized()
#	var rotation = Vector3(input_dir.x, 0, input_dir.y).normalized()
	print ("input dir = ", input_dir)
	return direction


func animation_change():
	var animation = anim_player.get_animation(anim_player.current_animation)
	
	if animation:
		animation.track_set_key_value(2, 0, anim_rows[sprite_facing])
	else: 
		printerr("Animation " + animation + " not found. " + get_parent().name)
		return


func update_anim_to_camera():
	var camera_fwd = main_camera.global_transform.basis.z
	print("camera_fwd = ", camera_fwd)
	var char_forward = global_transform.basis.z #Vector3(input_dir.x,0,1)
	print("char_fwd = ", char_forward)
	var char_left = global_transform.basis.x #Vector3(0,0,input_dir.y)
	print("char_left = ", char_left)
	
	var l_dot = char_left.dot(camera_fwd)
	print("l_dot = ", l_dot)
	var f_dot = char_forward.dot(camera_fwd)
	print("f_dot = ", f_dot)
	
	if f_dot < -0.65:
		sprite_facing = "front"
	elif f_dot > 0.65:
		sprite_facing = "back"
	else:
		if abs(f_dot) < 0.3 and l_dot > 0:
			sprite_facing = "right" # left sprite
		elif abs(f_dot) < 0.3 and l_dot < 0:
			sprite_facing = "left" # left sprite
		elif f_dot < 0 and l_dot > 0:
			sprite_facing = "frontright" # forward left sprite
		elif f_dot < 0 and l_dot < 0:
			sprite_facing = "frontleft" # forward left sprite
		else:
			if l_dot < 0:
				sprite_facing = "backleft" # back left sprite
			else:
				sprite_facing = "backright"


func update_sprite_facing():
	if input_dir.y < 0 and input_dir.x == 0:
		sprite_facing = "back"
	elif input_dir.y > 0 and input_dir.x == 0:
		sprite_facing = "front"
	elif input_dir.x < 0 and input_dir.y == 0:
		sprite_facing = "left"
	elif input_dir.x > 0 and input_dir.y == 0:
		sprite_facing = "right"
	elif input_dir.y < 0 and input_dir.x < 0:
		sprite_facing = "backleft"
	elif input_dir.y > 0 and input_dir.x > 0:
		sprite_facing = "frontright"
	elif input_dir.y > 0 and input_dir.x < 0:
		sprite_facing = "frontleft"
	elif input_dir.y < 0 and input_dir.x > 0:
		sprite_facing = "backright"

func update_front_direction(input_dir):
	if input_dir != Vector2.ZERO:
		front = Vector3(input_dir.x, 0, input_dir.y)
		front = front.normalized()

Bumping this thread. Still need help with this one

I’m going to look at this tomorrow when I’m less tired, but I’m 90% sure the issue stems from the fact that the front variable doesn’t actually do anything? And your 3D sprite isn’t actually rotating, so the basis.z and basis.x are always the same? If you front.z and front.x, the I think it should know what direction the sprite is facing and it can perform the correct calculation?

I think that would make it so it would continue to face the correct direction after moving, unsure about if the camera moves.

I’ve tried using the front variable, as well as some other hacks such as determining the character’s front using a separate child node (meshinstance set to a small sphere mesh for visibility and ease of debug) that is slightly off-center and which position changes according to where the sprite’s front is.

None of it worked, and I think the reason for this is the simple fact that relative to the camera, the basis of a slightly off-center node or a “front” that is +/-1 on z or x axes gives pretty much the same or very similar dot_product to the not “rotated” sprite. So for the camera basically nothing changes much, even if I switch what the front is.

In the end, I gave up trying to make it work and achieved the result I wanted by completely removing the dot products calculations and instead storing every direction of the sprite in an array with variables determining what left and right sprites should be relative to what the current sprite is.
And then I simply tied the switching logic to the same inputs that handle camera rotation.

Video

Related functions

func update_anim_to_camera():

if Input.is_action_just_pressed("camera_rotate_left"):
	sprite_facing = facings[next_left_facing] # back left sprite
elif Input.is_action_just_pressed("camera_rotate_right"):
	sprite_facing = facings[next_right_facing]
elif Input.is_action_pressed("camera_rotate_left") and Input.is_action_pressed("camera_rotate_right"):
		sprite_facing = "back"

func next_facings_calculation():

current_facing = facings.find(sprite_facing)
next_left_facing = (current_facing - 1 + facings.size()) % 8
next_right_facing = (current_facing + 1) % 8

This works, for the time being, but I don’t think it’s ideal.
For example, I have input that “resets” the camera to the initial position and when the sprite change was calculated through the dot product, I got a nice cycle of all directions while the animation of the camera returning to its original position played.

Now the sprite simply “rotates” together with the camera which felt wrong, so I changed it to reset to “back” as a compromise that, while it doesn’t feel like a bug, it also doesn’t look as cool as the cycle I had before.

So maybe there is a better way of doing this.

That’s pretty cool. Sorry I can’t handle long explanations.

But that video is a good start.

It seems like when you move the character sprite is correct relative to the camera, but the resting sprite relative to the camera is not.

Like the player object is never rotated.

If you want the player to face the direction they last moved in then you should rotate the player, not the sprite animation relative to the camera.

I have a similar input mechanism, I had to setup a custom signal to send a new basis state from the camera whenever it moved. The player consumed that basis to calculate movement input in the appropriate directions.