phantom-camera
phantom-camera copied to clipboard
Minimum Follow Distance for Follow Path Cameras in 3D
Project Type
3D
Feature Description
https://github.com/ramokz/phantom-camera/assets/85686969/16e8c61b-998a-4b46-8941-780758445f4d
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
https://github.com/ramokz/phantom-camera/assets/85686969/892aecd2-56a7-4a47-88c5-6e57c430613d
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
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.
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
.
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.
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?
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.
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.
https://github.com/ramokz/phantom-camera/assets/85686969/a9d93e36-fb41-456a-b05f-67ab8f5dcfc2
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.
Think there's a way to do this, but the scenario of how a camera will remain a minimum distance away when the follow_target
is approaching it and the camera reaches the end of the Path3D
is the main question. My guess is that it would likely have to curve around and above the Path3D
, like in the graph above, to get around that.
Also wonder if that would lead to a scenario where you could never have the camera on the other side of the player on the Path3D
if it has to abide by that rule? It might be a case of having multiple PCams
set up on either end to switch between to get around that, but it does start to get a bit complicated.
Hello! Sincere apologies for the delay, I appreciate the issue remaining open.
I get your concerns I think, though I also agree this is down to implementation as many of the games referenced had moments where the camera could swing wildly if several factors stacked together.
One thing in particular that drew me to phantom camera and might help here were the different types of follow mode. In my case what I was planning to do near transitions is set biases between a mixture of the player character mesh, the forward facing target and a node3D that was the intended angle. In other use cases (like the more static cameras of a Resident Evil game or Alisa) the forward facing target would be unused or not needed.
Not sure if this helps at all, thank you again for looking into this