Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Minimum Follow Distance for Follow Path Cameras in 3D #256

Open
PerdurantGames-Wyatt opened this issue Apr 23, 2024 · 6 comments
Open

Minimum Follow Distance for Follow Path Cameras in 3D #256

PerdurantGames-Wyatt opened this issue Apr 23, 2024 · 6 comments
Labels
3D Issues concerning 3D scenes enhancement New feature or request phantom camera Related to PhantomCamera nodes

Comments

@PerdurantGames-Wyatt
Copy link

PerdurantGames-Wyatt commented Apr 23, 2024

Project Type

3D

Feature Description

example.mp4

Currently when using a path follow type it locks to the target and attempts to use the minimum possible distance between the target, it and the path. This generally works well but in my attempts to use phantom camera's set to path follow as the baseline of a realtime 3D survival-horror project the current implementation creates unflattering angles and feels very artless. I've attempted a few workarounds but am unsure if this is possible in the current framework as the core issue is that "lock-on" being absolute rather than a minimum distance it then tries to seek on the path.

Use Cases

Primarily focused around projects that require fixed camera angles along paths but also have some degree of player control or dynamic movement coded in the path. Example games would Silent Hill and Metal Gear Solid trilogies on PS1 and PS2, as well as Ico.

https://youtu.be/k7Kr15i7qeI?t=1980

This video showcases a section in Silent Hill 2. The camera system in the hallway is able to handle rotation alongside some sharp cuts (already supported) or static angles while also keeping a minimum distance from the protagonist/characterbody3D before needing to find a different angle.

Importance

High - there are critical things I can't do without this feature

Usage

Often - a significant amount of projects can find this useful

(Optional) Proposed Solution

While done in a completely separate engine (Unreal in this case) and shown via visual scripting, this forum discussion goes over how users of that engine were able to implement a similar camera system to what I am describing and may provide insights.

https://forums.unrealengine.com/t/how-to-camera-movement-silent-hill-hallway-style/1166190/2

@PerdurantGames-Wyatt
Copy link
Author

camera_better.mp4

Was able to do an independent study/exploration outside of phantom camera to achieve the effect, but I would like any guidance (if appropriate/possible) on integrating some of the discoveries here with some of the benefits of phantom camera (volumes, framing, blending focus between multiple nodes)

On the player, the raycasts provide this information:

 
@export var FORWARD_SPEED = 2.0
@export var BACK_SPEED = 1.0
@export var TURN_SPEED = 0.025

@onready var raycast_forward = $raycast_forward
@onready var raycast_backward = $raycast_backward
var raycast_forward_default = null
var raycast_backward_default = null
@export var forward_target = Vector3.ZERO
@export var backward_target = Vector3.ZERO

var Vec3Z = Vector3.ZERO

func _ready():
	raycast_forward_default = raycast_forward.target_position
	raycast_backward_default = raycast_backward.target_position

#OPTIONAL: These could be used to change sensitivity of either rotating z or y
#var M_LOOK_SENS = 1
#var V_LOOK_SENS = 1

func _physics_process(_delta: float) -> void:
	if raycast_forward.is_colliding():
		raycast_forward.target_position = to_local(raycast_forward.get_collision_point())
	else:
		raycast_forward.target_position = lerp(raycast_forward.target_position,raycast_forward_default,.05)
	if raycast_backward.is_colliding():
		raycast_backward.target_position = to_local(raycast_backward.get_collision_point())
	else:
		raycast_backward.target_position = lerp(raycast_backward.target_position,raycast_backward_default,.05) 
	forward_target = to_global(raycast_forward.target_position)
	backward_target = to_global(raycast_backward.target_position)
	
	if Input.is_action_pressed("move_forward") and Input.is_action_pressed("move_back"):
		velocity.x = 0
		velocity.z = 0

	elif Input.is_action_pressed("move_forward"):
		var forwardVector = -Vector3.FORWARD.rotated(Vector3.UP, rotation.y)
		velocity = -forwardVector * FORWARD_SPEED
		
	elif Input.is_action_pressed("move_back"):
		var backwardVector = Vector3.FORWARD.rotated(Vector3.UP, rotation.y)
		velocity = -backwardVector * BACK_SPEED
	
	#If pressing nothing stop velocity
	else:
		velocity.x = 0
		velocity.z = 0
	
	# IF turn left WHILE moving back, turn right
	if Input.is_action_pressed("turn_left") and Input.is_action_pressed("move_back"):
		rotation.z -= Vec3Z.y + TURN_SPEED #* V_LOOK_SENS
		rotation.z = clamp(rotation.x, -50, 90)
		rotation.y -= Vec3Z.y + TURN_SPEED #* M_LOOK_SENS
	
	elif Input.is_action_pressed("turn_left"):
		rotation.z += Vec3Z.y - TURN_SPEED #* V_LOOK_SENS
		rotation.z = clamp(rotation.x, -50, 90)
		rotation.y += Vec3Z.y + TURN_SPEED #* M_LOOK_SENS

	# IF turn right WHILE moving back, turn left
	if Input.is_action_pressed("turn_right") and Input.is_action_pressed("move_back"):
		rotation.z += Vec3Z.y - TURN_SPEED #* V_LOOK_SENS
		rotation.z = clamp(rotation.x, -50, 90)
		rotation.y += Vec3Z.y + TURN_SPEED #* M_LOOK_SENS
		
	elif Input.is_action_pressed("turn_right"):
		rotation.z -= Vec3Z.y + TURN_SPEED #* V_LOOK_SENS
		rotation.z = clamp(rotation.x, -50, 90)
		rotation.y -= Vec3Z.y + TURN_SPEED #* M_LOOK_SENS
	
	move_and_slide()

and the test camera

extends Path3D

@onready var path = self
var world = null
var player = null
@onready var camera_3d = $"../Camera3D"

func find_closest_abs_pos(
	path: Path3D,
	global_pos: Vector3
	):
	var curve: Curve3D = path.curve

	# transform the target position to local space
	var path_transform: Transform3D = path.global_transform
	var local_pos: Vector3 = global_pos * path_transform

	# get the nearest offset on the curve
	var offset: float = curve.get_closest_offset(local_pos)

	# get the local position at this offset
	var curve_pos: Vector3 = curve.sample_baked(offset, true)

	# transform it back to world space
	curve_pos = path_transform * curve_pos
	
	return curve_pos

# Called when the node enters the scene tree for the first time.
func _ready():
	world = get_parent_node_3d()
	if world.has_node("Character_Player"):
		player = world.get_node("Character_Player")
	else:
		print("player not found")

func _physics_process(_delta):
	var target = Vector3(0,0,0)
	var previous_target = Vector3(0,0,0)
	
	if player != null:
		previous_target = player.forward_target
		
		target = player.forward_target
		
		camera_3d.transform.origin = find_closest_abs_pos(self,lerp(camera_3d.transform.origin,player.backward_target,.95))
	
	camera_3d.look_at(lerp(previous_target,target,.95),Vector3.UP,false)

Some code shown is from the following sources:

https://godotforums.org/d/36094-3d-tank-controls-for-player-movement

https://medium.com/@oddlyshapeddog/finding-the-nearest-global-position-on-a-curve-in-godot-4-726d0c23defb

@ramokz ramokz added enhancement New feature or request phantom camera Related to PhantomCamera nodes 3D Issues concerning 3D scenes labels May 15, 2024
@ramokz
Copy link
Owner

ramokz commented May 15, 2024

Have been giving this some thought, and think this should be fairly durable.

The only question I guess is whether if the applied minimum distance should always be applied to the camera's local z-axis so that it pushes the camera away from its follow target in the opposite direction it's facing the Path3D? Can imagine it might lead to some potential quirky, and maybe sporadic, camera movement if the player gets too close, or even crosses, the follow path line. Though that might not be an issue if set up and applied correctly based on the playable character's mobility.

@ramokz
Copy link
Owner

ramokz commented May 15, 2024

Practically speaking, it should be a matter of applying a positional value to the FollowMode.PATH section in the PhantomCamera3D script. Where it compares a newly defined property, e.g. min_path_distance, with the Vector3 distance between the calculated follow_position and the follow_target's global position. If the min_path_distance is greater than the calculated distance between follow_position and the follow_target's global position then it will apply that difference on top of the follow_position.

@PerdurantGames-Wyatt
Copy link
Author

I haven't fully solved that one myself in regards to the quirky and sporadic movement, at least with the implementation I've been assisted with. A lot of does come down to the designer/editor being mindful of the placement. The source examples and era I'm drawing from do have a lot of infamy with cameras being a bit chaotic and unwieldly so it's not too bad I feel if that remains a caveat.

One of the few potential resolutions I've been tempted to include but have yet to due to wanting to piggypack on Phantom Camera functionality is a bias/weighting system where there are rays also exiting from the sides of the character and the final camera position is based on trying to follow a few different positions or markers like the group functionality does in Phantom Camera currently. I think that, leveraged with a way to set a 180 degree bias plane would help keep the camera from making wild swings and make fairly natural / expected decisions, but I am unsure and it is necessary for the baseline integration.

If you would like, significant improvements have been made on the code referenced above have been made if you think it would help in integrating the feature. I'll also try to implement things myself based on your description but am fairly new to programming so am unsure how clean it will come out.

@ramokz
Copy link
Owner

ramokz commented May 16, 2024

Wonder if a simple solution would be to have the camera offset itself from the path when the followed target gets too close, like so?

image

Essentially, it's a bit of simple maths and trigonometry to push the camera away from the followed target if the distance between it and the Path point is less than the dev defines the min distance as. An obvious issue will occur when the player moves through the Path node or gets too close to the Path, which can result in clipping through walls etc. But to your point, think that is something the developer needs to consider when setting it up the Path's position and potentially applying restrictions to the target's movement.

@PerdurantGames-Wyatt
Copy link
Author

Sorry for the delay on this! Yes, I agree and think/was hoping for the path to be a broad suggestion more often than being rigidly defined, at least by default. I think my delay came from wanting to grab better video of the current implementation I have, and where I wouldn't mind improvement.

debug_video.mp4

The camera right now is locked to the player's back essentially, which generally flows pretty good but when trying to turn where the debug figure is or in other scenarios, rather than fall back to where it has better coverage of the player, the front facing object and it's rear target position, it sacrifices everything for the front view.

also, godot was being a little stubborn so here's a rough traceover of the path rather than the godot markers. With the red line being the path3d's charted route, gold being the current position and green being the ideal position.

Snag_dff1c5

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3D Issues concerning 3D scenes enhancement New feature or request phantom camera Related to PhantomCamera nodes
Projects
None yet
Development

No branches or pull requests

2 participants