I'm trying to make a character's eye bones look at a target, as well as the head bone. Making the eyes look at a target OR the head works in isolation but when I try to use one LookAt script for the head and simultaneously a LookAt script for the eyes, the eyes do not follow the head bone. I've been working on this for the entire day today and I can't for the life of me figure out how to do this.

Does anyone know the proper way to handle this? It's utterly confusing. What is the bone pose relative to? the skeleton? locally? globally? its own thing relative to the object space or some such? Do I need to traverse the parent hierarchy to multiply all the transforms?

I seemingly tried all of that but to no avail, either the head and eye bones end up placed and some extremely far away position, stretching several meters, or other times they end up staying at the initial position and fail to follow the proper bone position of the parent.

So I've been working at this for about a whole day and one morning. Finally, I can present my BoneLookAt script!

It supports a custom offset rotation as well as minimum and maximum rotation limits:

@tool
class_name BoneLookAt extends Marker3D

@export var bone_name:String
@export var interpolation:float = 1.0
@export var use_external_skeleton := false
@export var external_skeleton:NodePath

@export var follow_target:NodePath
@export var follow_threshold_degrees:float = 15

@export var offset_rotation:= Vector3(0, 0, 0)
@export var min_rotation := Vector3(-180, -180, -180)
@export var max_rotation := Vector3(180, 180, 180)

@export var limits_enabled:bool = false

@export var enabled := false:
set(e):
if enabled and not e:
enabled = e
should_reset = true
else:
enabled = e

var should_reset := false

var last_debug:int = 0

func get_skeleton() -> Skeleton3D:
if use_external_skeleton:
return get_node(external_skeleton)
else:
return get_parent()

func should_follow_target(current_dir: Vector3, target_dir: Vector3) -> bool:
var angle = acos(current_dir.normalized().dot(target_dir.normalized())) * 180 / PI
return angle > follow_threshold_degrees

func _move_towards_target(weight:float = 0.1):
var target = get_node(follow_target)
if target:
global_position = lerp(global_position, target.global_position, weight)

func to_radians(euler:Vector3):
return Vector3(deg_to_rad(euler.x), deg_to_rad(euler.y), deg_to_rad(euler.z))

func _process(delta):
var debug = false
var t = Time.get_ticks_msec()
if t - last_debug > 500:
debug = true
last_debug = t

var skeleton:Skeleton3D = get_skeleton()
if not skeleton:
	return

var bone_idx:int = skeleton.find_bone(bone_name)
if bone_idx == -1:
	return

var bone_pose : Transform3D = skeleton.get_bone_pose(bone_idx)

if not enabled:
	if should_reset:
		var global_pose_no_override = skeleton.get_bone_global_pose_no_override(bone_idx)
		skeleton.set_bone_global_pose_override(bone_idx, global_pose_no_override, interpolation, false)
		should_reset = false
	return

var current_dir = bone_pose.origin.direction_to(global_position)
var target_dir = bone_pose.origin.direction_to(get_node(follow_target).global_position)

if follow_target:
	if should_follow_target(current_dir, target_dir):
		_move_towards_target()
	else:
		_move_towards_target(0.01)

var parent_bone_idx = skeleton.get_bone_parent(bone_idx)
if parent_bone_idx > -1:
	var parent_global_pose = skeleton.get_bone_global_pose(parent_bone_idx)
	bone_pose = parent_global_pose * bone_pose

var global_bone_pose = bone_pose

global_bone_pose = global_bone_pose.looking_at(skeleton.to_local(global_position), Vector3.UP, true)

# add rotation limits
if limits_enabled:
	var euler:Vector3 = global_bone_pose.basis.get_euler()
	
	var min_rot = to_radians(min_rotation)
	var max_rot = to_radians(max_rotation)
	
	if euler.x < min_rot.x:
		euler.x = min_rot.x
	elif euler.x > max_rot.x:
		euler.x = max_rot.x
	
	if euler.y < min_rot.y:
		euler.y = min_rot.y
	elif euler.y > max_rot.y:
		euler.y = max_rot.y
	
	if euler.z < min_rot.z:
		euler.z = min_rot.z
	elif euler.z > max_rot.z:
		euler.z = max_rot.z
	
	global_bone_pose.basis = Basis().from_euler(euler)

# add the offset rotation
global_bone_pose = global_bone_pose.rotated_local(Vector3(1, 0, 0), deg_to_rad(offset_rotation.x))
global_bone_pose = global_bone_pose.rotated_local(Vector3(0, 1, 0), deg_to_rad(offset_rotation.y))
global_bone_pose = global_bone_pose.rotated_local(Vector3(0, 0, 1), deg_to_rad(offset_rotation.z))

skeleton.set_bone_global_pose_override(bone_idx, global_bone_pose, interpolation, true)

This is very interesting. If you put ``` above and below the code, it will look much better.

```
code
```