• Godot Help3D
  • 4.4.1 - Camera jitters when colliding with geometry

Hi all, I'm really scratching my head here.

I have my camera setup so that is a third-person controlled camera, with response to collisions. It is set up via a gimbal setup, with a SpringArm3D node and a Camera3D node.

My player is a CharacterBody3D. The camera setup is a child of the player, but is set as top level so it doesn't inherit position/rotation information.

The problem that I'm having is that the camera movement as it follows the player is very smooth, so long as I'm not colliding with something. To illustrate the issue, I attached a bright green sphere as a child of the camera, so that you can see how it behaves here:

I am using Jolt 3D, I am using physics interpolation, and all nodes for the camera setup are set up to not use interpolation.

Any ideas what could be going on here? Any guidance is greatly appreciated! This is my camera control code:


# 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
## The parent that should not be collided against
@export var excluded_parent : NodePath

# Onready vars here
@onready var spring_arm = $"Pivot/SpringArm3D"
@onready var camera = $"Pivot/Camera3D"
@onready var marker = $"Pivot/SpringArm3D/CamHelper"
@onready var pivot = $"Pivot"

# Other variables here
var can_clip : bool = false
var target

func _ready() -> void:
	Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
	
	if follow_target and excluded_parent:
		target = get_node(follow_target)
		spring_arm.add_excluded_object(get_node(excluded_parent))
		
	set_as_top_level(true)
	set_physics_interpolation_mode(Node.PHYSICS_INTERPOLATION_MODE_OFF)

func _unhandled_input(event: InputEvent) -> void:
	# Handle rotation of the camera here
	if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
		mouse_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)

@warning_ignore("unused_parameter")
func _process(delta: float) -> void:
	_follow_target()
	_check_collision()
	_rotate_camera()
	_clamp_rotation()
	
func _follow_target():
	var tr : Transform3D = target.get_global_transform_interpolated()

	global_position = lerp(global_position, tr.origin, 0.05)

	
func _check_collision() -> void:
	# A hit, a hit, a palpable hit!
	if spring_arm.get_hit_length() < spring_arm.spring_length:
		can_clip = true
	else:
		can_clip = false
	
	# How to respond in a collision
	if can_clip:
		if camera.transform.origin.z > marker.transform.origin.z:
			camera.transform.origin.z = marker.transform.origin.z
		elif camera.transform.origin.z < marker.transform.origin.z:
			camera.transform.origin.z = lerp(camera.transform.origin.z, marker.transform.origin.z, 0.05)
	else:
		camera.transform.origin.z = lerp(camera.transform.origin.z, marker.transform.origin.z, 0.02)

func _rotate_camera():
	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)

func mouse_rotate_camera(event: InputEventMouseMotion) -> void:
	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()

func _clamp_rotation() -> void:
	pivot.rotation.x = clamp(pivot.rotation.x, min_vertical_angle, max_vertical_angle)
	rotation.y = wrapf(rotation.y, 0.0, TAU)```
  • xyz replied to this.
  • Ok, I am making a new post to cover what the actual problem was.

    The issue was with my code that checked the status of the springarm to determine when the camera should update.

    Because I wanted the camera to instantly snap forward, the camera would "jitter" during collisions, because as the gimbal was lerping to the player the camera was then also trying to update its position again.

    The solution was simple, and everything is now properly decoupled from the physics tick, as it should be.

    All I did was this:

    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 = lerp(camera.position.z, marker.position.z, 25.0 * delta)
    		elif camera.position.z < marker.position.z:
    			camera.position.z = lerp(camera.position.z, marker.position.z, 3.5 * delta)
    			
    
    	camera.position.z = lerp(camera.position.z, marker.position.z, 5.0 * delta)

    What this does is instead of updating the camera to the marker's position every frame, it will instead lerp (but quickly) to it. This removed the jitter, and the camera still pulls forward at a decent pace.

    I also made it so that if the camera is still colliding with geometry behind the player, it will smoothly zoom out backwards.

    And finally, the last line is outside of the collision check, so that as soon as the camera is done colliding, it will pull back to the marker's position.

    Here is a properly designed demo project, the player now rotates and properly takes the camera direction into consideration as it moves. WASD, mouse to rotate, and ALT + Q to quit the game as it will be in full screen.

    IMPORTANT NOTE: This project was done in 4.5 beta 2 to see if that had any bearing on my original issue, but it didn't. It turns out my issue is just poor code design (who would've thought!).

    jitter-free-mrp.zip
    15kB

    xyz This might be a dumb question, but what do you mean by "after"?

    Ah, I may have found the solution.

    I put all of my logic inside of _physics_process(), and moved my _follorw_target() call to the end of that, and this seems to have done the trick. No more jitter of any kind.

    My one concern is that I am using physics interpolation, so input on the camera might feel a little off, but this could just be me being picky!

    can you upload minimal project here? delete .godot directory and zip everything else.

    I cant remember but i have similar setup and i dont remember having this issue, and i dont use any collision checks, you just add shape to the spring arm and it does what it does.

      kuligs2 Can confirm it jitters on 4.5.dev3 build
      I dont have fancy code to set the springarm. I have collisionshape or shape in the springarm and its shepre 3d

      If you slowly pan the springarm towards a surface and it reels in then it jitters. If you do it fast then its not noticable..

      Would be nice to have no jitter

      xyz Try updating the camera after the springarm node has been processed.

      In my case my camera is a child of springarm.

        kuligs2 Your jitter looks more like an issue with mouse movement than actual jitter. I solved this by following the advice in this article.

        I am also using Jolt Physics, and physics interpolation, which may also help you out there.

        I am also indirectly using the SpringArm to control the camera, by having a helper node be a child of the SpringArm, and setting the camera's position to that of the helper. This allows me to have a very smooth return, but an instant position update if the player is blocked.

          Jerstopholes it might look like mouse movement problem but its not, in some parts of the video i moved the spring arm up a bit so that it dont collide and moved my mouse slowly just as i did when it was colliding.

          i too use jolt, i think its bydefault since 4.4 or something. I will have to check.

            kuligs2 I have noticed in my experimentation that depending on what shape you use for the SpringArm, you will get different results, which makes sense.

            For me, I am using a SphereCast3D shape in the SpringArm, instead of the default empty/no shape. It makes a big difference, that could well be the jitter you're seeing.

              Jerstopholes im using Sphere shape, dont remember the correct name for it, not sure if it was "cast". When using no collision shape in spring arm, it starts to clip through walls, even tho all the settings are set not to.

                kuligs2 I will try to post a repro project tonight. It's interesting that we are having wildy different results!

                I also did notice that although jittery is significantly better in my project, there are places where it is still visible. It's very limited and barely noticeable, but is still there. So my solution of putting everything in _physics_process may not be the best.

                xyz and @kuligs2 OK, I have a reproduction project here.

                jitter-mrp.zip
                12kB

                Here is a video showing the jitter I am still experiencing.

                My setup is as follows:

                The player is a CharacterBody3D. I am using physics interpolation, and Jolt physics.

                I have a camera setup using a gimbal and a SpringArm3D. The SpringArm3D has a position marker, CamHelper. The Camera 3D is fed the information from CamHelper to update its position, it does not follow the SpringArm3D node directly.

                Take a look at "TP Camera" as a scene, and look at the script it uses to get an idea of how I am doing the collision checks.

                Notice that as the camera clips into geometry and the player pushes itself closer to the camera, there is still a distinct jitter effect. I am uncertain as to what is causing this.

                If I put all of my logic into _physics_process(), the jitter is greatly reduced, but not completely gone. That also goes against all advice I've been given on decoupling a camera from physics altogether, so that it is always a smooth experience for every player.

                Any pointers would be great!

                EDIT: I made an edit to my camera setup, and got rid of any lerping I was doing, and made the Camera3D a direct child of SpringArm3D, and still experiencing the jitter that @kuligs2 is seeing... so it appears this may be an issue with the SpringArm node? I will try using a ShapeCast3D node and see if I get different results, but I sort of doubt it since that's essentially how the SpringArm node works anyway.

                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.

                new-jitter-mrp.zip
                15kB

                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)

                Ok, I am making a new post to cover what the actual problem was.

                The issue was with my code that checked the status of the springarm to determine when the camera should update.

                Because I wanted the camera to instantly snap forward, the camera would "jitter" during collisions, because as the gimbal was lerping to the player the camera was then also trying to update its position again.

                The solution was simple, and everything is now properly decoupled from the physics tick, as it should be.

                All I did was this:

                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 = lerp(camera.position.z, marker.position.z, 25.0 * delta)
                		elif camera.position.z < marker.position.z:
                			camera.position.z = lerp(camera.position.z, marker.position.z, 3.5 * delta)
                			
                
                	camera.position.z = lerp(camera.position.z, marker.position.z, 5.0 * delta)

                What this does is instead of updating the camera to the marker's position every frame, it will instead lerp (but quickly) to it. This removed the jitter, and the camera still pulls forward at a decent pace.

                I also made it so that if the camera is still colliding with geometry behind the player, it will smoothly zoom out backwards.

                And finally, the last line is outside of the collision check, so that as soon as the camera is done colliding, it will pull back to the marker's position.

                Here is a properly designed demo project, the player now rotates and properly takes the camera direction into consideration as it moves. WASD, mouse to rotate, and ALT + Q to quit the game as it will be in full screen.

                IMPORTANT NOTE: This project was done in 4.5 beta 2 to see if that had any bearing on my original issue, but it didn't. It turns out my issue is just poor code design (who would've thought!).

                jitter-free-mrp.zip
                15kB