EDIT: This actually did not solve the problem, and is definitely not the intended way to bypass jitter. I am unmarking it as the best answer.
I discovered the source of my jitter, and feel really silly that I didn't notice it sooner.
Basically, because of the way I wrote my collision check logic, the camera will, on one frame, be ahead of the CamHelper
node, and on the next frame be behind it, resulting in the jitter.
I somehow have to write my code better. D'oh!
ORIGINAL:
I have managed to reduce the jitter to a tolerable level. It took some doing, but I got there. It is now only barely noticeable when the camera starts to intersect the player in very tight spaces, tighter than what would be considered good level design.
First, I kept my setup pretty much the same, except I now have physics interpolation on for the camera gimbal, and updated my script to now do the following and collision checks in _physics_process()
, and now only apply rotations in _process()
, instead of doing everything in _process()
.
And the revised MRP is here, for those interested to check out.
I also made sure that any values that need to be frame-independent were properly multiplied by delta. This is the revised script.
# Variables to expose here
## Mouse Sensitivity
@export var mouse_sensitivity : float = 100.0
## Minimum viewing angle (in degrees)
@export_range(-90.0, 0.0, 0.1, "radians_as_degrees") var min_vertical_angle: float = -PI/2
## Maximum viewing angle (in degrees)
@export_range(0.8, 90.0, 0.1, "radians_as_degrees") var max_vertical_angle: float = PI/4
## The target the camera is to follow
@export var follow_target : NodePath
# Onready vars here
@onready var camera = %Camera3D
@onready var marker = %Marker3D
@onready var spring_arm = %SpringArm3D
@onready var pivot = $"Pivot"
var can_clip : bool = false
var target : Node
var target_pos : Vector3 = Vector3()
var cam_offset : Vector3 = Vector3()
func _ready() -> void:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
if follow_target:
target = get_node(follow_target)
spring_arm.add_excluded_object(get_node(follow_target))
cam_offset = global_position - target.global_position
set_as_top_level(true)
# Force physics interpolation ON
set_physics_interpolation_mode(Node.PHYSICS_INTERPOLATION_MODE_ON)
# Force VSync on
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ENABLED)
func _unhandled_input(event: InputEvent) -> void:
# Handle rotation of the camera here
if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
_rotate_camera(event)
# Release/capture the mouse
if event.is_action_pressed("ui_cancel"):
if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
else:
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
# Quick Quit (ALT + Q)
if event.is_action_pressed("quick_quit"):
get_tree().quit()
# Collision check and follow target
func _physics_process(delta):
_check_collision(delta)
_follow_target(delta)
@warning_ignore("unused_parameter")
func _process(delta):
_rotate_camera(InputEventJoypadMotion)
func _check_collision(delta):
can_clip = spring_arm.get_hit_length() < spring_arm.spring_length
if can_clip:
if camera.position.z >= marker.position.z:
camera.position.z = marker.position.z
elif camera.position.z < marker.position.z:
camera.position.z = lerp(camera.position.z, marker.position.z, 3.5 * delta)
else:
camera.position.z = lerp(camera.position.z, spring_arm.spring_length, 5.0 * delta)
func _follow_target(delta):
var tf : Transform3D = target.get_global_transform_interpolated()
target_pos = lerp(target_pos, tf.origin + cam_offset, 5.0 * delta)
global_position = target_pos
func _rotate_camera(event) -> void:
if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
var viewport_transform: Transform2D = get_tree().root.get_final_transform()
var motion: Vector2 = event.xformed_by(viewport_transform).relative
var degrees_per_unit: float = 0.001
motion *= mouse_sensitivity
motion *= degrees_per_unit
# Don't apply rotation if we're not moving the mouse
if is_zero_approx(motion.length() > 0.01):
return
# Apply pitch
pivot.rotate_object_local(Vector3.LEFT, deg_to_rad(motion.y))
pivot.orthonormalize()
# Apply yaw
rotate_object_local(Vector3.DOWN, deg_to_rad(motion.x))
orthonormalize()
if Input.mouse_mode == Input.MOUSE_MODE_VISIBLE:
var input_vector = Input.get_vector("look_right", "look_left", "look_down", "look_up")
rotate_y(input_vector.x * 0.05)
pivot.rotate_x(input_vector.y * 0.05)
# Clamp the rotation
pivot.rotation.x = clamp(pivot.rotation.x, min_vertical_angle, max_vertical_angle)
rotation.y = wrapf(rotation.y, 0.0, TAU)