Align To Normal With Raycast Question

Godot Version

4.3

Question

I’m using a raycast attached at the forward of a CharacterBody to get the normal of a face, and then adjust the players basis to the normal. So they can effectively walk on walks, or ceilings. It’s working alright except at a few specific angles that completely don’t line up.

I’m wondering what I’m doing wrong, and would appreciate help.

Also if anyone could recommend a direction on where I should hit the books, and what I fundamentally need to understand and learn to actually grasp what’s going on here. I’ve been wracking my brain at these walls I keep hitting, so I’ve been re-learning math from the ground up to hopefully get into the more complicated subjects of Algebra, and further eventually. I understand the more complicated stuff I wanna do, this is inevitable.

Node Structure

Code

extends CharacterBody3D

@export var move_speed = 5.0
@export var jump_height = 5.0
@export var turn_speed = 5.0

const SPEED = 5.0
const JUMP_VELOCITY = 4.5

@onready var wall_change_detection: RayCast3D = %WallChangeDetection
@onready var wall_change_detection_2: RayCast3D = %WallChangeDetection2
@onready var wall_change_detection_3: RayCast3D = %WallChangeDetection3

@onready var player_camera: PlayerCamera = $PlayerCamera
@onready var mesh: MeshInstance3D = $Mesh

var on_wall : bool = false
var on_ceiling : bool = false
var upright : bool = true

var transitioning : bool = false

var xform : Transform3D

func _unhandled_input(event: InputEvent) -> void:
	if Input.is_action_just_pressed("interact"):
		global_transform = align_with_normal(global_transform, Vector3(0,0,-1).normalized())
		
func _physics_process(delta: float) -> void:
	if wall_change_detection.is_colliding():
		global_transform = align_with_normal(global_transform, wall_change_detection.get_collision_normal().normalized())
	
	### Add the gravity.
	#if upright:
		#velocity.y += -5 * delta
	
	if Input.is_action_pressed("movement"):
		var dir = Input.get_vector("move_backward", "move_forward", "move_right", "move_left").normalized()
		dir = dir.rotated(player_camera.yaw_node.rotation.y)
		var target_rotation = atan2(dir.y, dir.x) - rotation.y
		mesh.rotation.y = lerp_angle(mesh.rotation.y, target_rotation, turn_speed * delta)
		translate(Vector3(-dir.y, 0, -dir.x) * move_speed * delta)
	
	move_and_slide()

func align_with_normal(xform, normal):
	
	xform.basis.x = -xform.basis.z.cross(normal)
	xform.basis.y = normal
	
	xform.basis = xform.basis.orthonormalized()
	
	return xform

I would potentially enable debug shapes to see if the visible mesh actually aligns with its collision shape. Or see if its hitting something invisible or a corner.

I dont see anything glaringly wrong, you could probably simplify by using the basis version of look_at function using the normal for up and the target could just be some arbitrary point in the same direction as the raycast. Or just the collision point.

Look_at will square the y to the z (according to the docs). While your method squares the y from the x during orthonormalize.

That just flips it a different way unfortunately unless I’m using it wrong.

I’m learning Quaternions, and Euler Angles at the moment to try and remedy this issue in the future. There’s a big lack of understanding on angles, and rotations here for me for what I’m trying to accomplish.

You can pass a third parameter to use the model front , or +z world axis for looking. A basis z axis by default is positive, but is really represents the -z direction in the real world. If you just say true for the third parameter it should un-flip it.

Atan2 is an interesting use. I like to just use vector basis multiplication to do input rotations. Although it could be cheaper to do it your way and also stay on a flat plane.

1 Like

I eventually got it working using that method using the second and third parameters that you mentioned, but I opted a different approach with similar results. Based on this post here(credit). With some tweaking, it’s moving in all the proper directions, and feels great so far; and no weird crashes when I change the basis anymore. :sweat_smile:

The issue now is that it’s falling apart at the mesh level with how I’m rotating it since I chose to rotate the mesh like this:


	if Input.is_action_pressed("movement"):
		var dir = Input.get_vector("move_backward", "move_forward", "move_right", "move_left").normalized()
		dir = dir.rotated(player_camera.yaw_node.rotation.y)
		var target_rotation = atan2(dir.y, dir.x) - rotation.y
		mesh.rotation.y = lerp_angle(mesh.rotation.y, target_rotation, turn_speed * delta)
		translate(Vector3(-dir.y, 0, -dir.x) * move_speed * delta)

Video

Current Code So Far:

extends CharacterBody3D

@export var move_speed = 5.0
@export var jump_height = 5.0
@export var turn_speed = 5.0

@onready var wall_change_detection: RayCast3D = %WallChangeDetection

@onready var player_camera: PlayerCamera = $PlayerCamera
@onready var mesh: MeshInstance3D = $Mesh


var transitioning : bool = false


func _input(event: InputEvent) -> void:
	if event.is_action_pressed("interact") and not transitioning:
		if wall_change_detection.is_colliding():
			align_to_new_normal(wall_change_detection.get_collision_normal(), wall_change_detection.get_collision_point())


func _physics_process(delta: float) -> void:
	
	### Add the gravity.
	#if upright:
		#velocity.y += -5 * delta
	
	if Input.is_action_pressed("movement"):
		var dir = Input.get_vector("move_backward", "move_forward", "move_right", "move_left").normalized()
		dir = dir.rotated(player_camera.yaw_node.rotation.y)
		var target_rotation = atan2(dir.y, dir.x) - rotation.y
		mesh.rotation.y = lerp_angle(mesh.rotation.y, target_rotation, turn_speed * delta)
		translate(Vector3(-dir.y, 0, -dir.x) * move_speed * delta)
	
	move_and_slide()

func align_to_new_normal(new_normal:Vector3, attach_point:Vector3):
	transitioning = true
	var original = self.transform.basis.y
	var cosa = original.dot(new_normal)
	var alpha = acos(cosa)
	var axis = original.cross(new_normal)
	axis = axis.normalized()
	
	var t = get_tree().create_tween()
	
	t.tween_property(self, "transform", self.transform.rotated(axis, alpha), 0.2)
	t.parallel().tween_property(self, "global_position", wall_change_detection.get_collision_point(), 0.2)
	t.tween_callback(func(): transitioning = false)

Nevermind, I’m an idiot, and stopped subtracting the rotation of the node’s y from the headed direction and it seems to be working flawlessly now. :sweat_smile: Have proper wall walking and moving. Now to make it look pretty and more functional.

BEFORE

var target_rotation = atan2(dir.y, dir.x) - rotation.y

AFTER

var target_rotation = atan2(dir.y, dir.x)