Sunday, May 3, 2015

Framing a target

I tend to update a game camera in this order:

  1. Update all possible camera targets
  2. Determine current target (“subject”)
  3. Apply change of camera mode or parameters
  4. Determine the camera facing direction
  5. Determine a camera position
  6. Collision and other post update operations.

I make exceptions to this order for cameras with restricted positioning, such as cameras aligned with splines, which is a topic for future posts. Although this site does not focus on VR there are additional order updating considerations for that too.

Placing a camera to frame a subject

Of these steps, the most basic one is probably determining the camera position. It is also more forgiving than camera rotation in terms of keeping track of distant objects on the screen. When rotating the camera a distant point will move farther on-screen than a closer point, but when translating the camera the distant point will move less than a closer point.

There are a lot of artistic choices when determining the best screen positioning of the target, and that should be a part of the camera design, this post is about the things that have more practical results for the player.

The first iteration is placing the target at the center of the screen with a fixed distance to the camera. If the reference point of the target is the bottom center, you’d now have a target standing on the center of the screen. At this stage we can start thinking about how the target should fit in the screen rather than the relationship between two objects in space.

So let’s pick some camera modes: A full body 3rd person and an over the shoulder camera. Each of these modes have some rules for where a specific point of the player should be on screen and each have some artistic options. Let’s say for the full body camera we want the feet to be aligned with 1/5th of the screen from the bottom and for the over shoulder camera the top of the head should be approximately at the center of the screen and the shoulders should be visible at the bottom of the screen.

We should have the full camera orientation at this point, we already decided on a forward direction so the up and side directions may need to be updated.

We’re missing the camera distance information for the full body camera but that is more of an artistic call so we can leave that for later and just pick something for now.

Example of a full body camera view and an over the shoulder camera view with screen alignments

Vertical camera positioning

To find the world plane that the vertical screen line at 1/5th from the bottom represents in the world we start with that the bottom to the top is the same as 3/5ths from the center to the bottom of the screen. We can find the tangent of the angle from the camera forward by using the vertical field of view:

tan_feet_plane = tan(fovvertical/2)*3/5

We can find the normal of the plane that the feet and the camera should share by:

nvertical = cos_feet_plane * up + sin_feet_plane * forward

Cosine and sine can be found from the tangent as long as we know the signs:

cos(v) = 1/√(tan2(v)+1)
sin(v) = tan(v) * cos(v)

Now all we need for the vertical positioning of the full body camera is to make sure that the dot product with the normal matches for both the feet and the camera position (n dot camera position = n dot feet center) and to do that the camera needs a horizontal constraint too.

Before jumping in to horizontal positioning let’s do the vertical planning for the over the shoulder camera as well.

In order to keep the over the shoulder camera distance consistent I would recommend keeping the top of the head to center of shoulder height a constant rather than evaluating it every frame. This means that the distance can be found by:

height = tan(v) * distance
distance = height / tan(v)

The angle in this case will be half the screen:
distance = height / tan(fov/2)

If the camera also has a pitch angle the distance will be off and we can compensate for that, but the center of the shoulder may be an awkward point to orbit around. Somewhere between the top of the head and center of shoulder may be better. Adjusting the distance based on the pitch angle may not be ideal either so we can split the difference and place the screen line at ¼ of the screen from the bottom and that should line up with the roughly the neck of the target, or the middle of the shoulder and top of head.

nvertical = cos_neck_plane * up + sin_neck_plane * forward

And similar to the full body camera we make sure the normal dot product with both the neck and the camera are the same value.

Horizontal camera positioning

For the horizontal positioning of the full body camera we could consider keeping the target centered by simply centering. This is as simple as using the side direction as a plane normal and lining that up with the feet:

side ᐧ camera position = side ᐧ feet position.

Let’s say we wanted the camera to look a little bit ahead in the direction the target is facing. There are some options involving adding an offset to the horizontal or vertical alignment planes, but it is easier to calculate an alternative position in front of the target, using its facing direction:

look position = feet position + look ahead distance * facing direction

Unfortunately if you plug that in the camera will be jumping around with the slightest facing change so the look ahead offset needs to be softened a bit. When it comes to softening something relating to camera movement the best thing to do is generally to base the change on the movement of the target:

look_ahead_offset_target = look_ahead_distance * facing
movement = blend_factor * target_frame_move / max_frame_move
look_ahead_offset += movement * (look_ahead_offset_target-look_ahead_offset)

What we’re calculating here is a scalar based on how much the target moved, where blend_factor is some constant you choose that is less than one, target_frame_move is the distance the target moved and max_frame_move is the maximum movement in a frame (or max speed divided by frame time).

We can also build in some tolerance for incidental movement. Say that we don’t want the camera to move unless the target moves with a certain minimum frame move:

target_frame_move >= min_frame_move:
movement = blend_factor * (target_frame_move-min_frame_move) / (max_frame_move-min_frame_move)

target_frame_move < min_frame_move:
look_ahead_offset -= target_frame_position_move

Unfortunately we’ll soon discover that the target can drift away from the camera when moving slower than min_frame_move so we need to limit the look_ahead_offset:

|| look_ahead_offset || > look_ahead_distance:
look_ahead_offset *= look_ahead_distance / || look_ahead_offset ||

You should also experiment with changing the min_frame_move if the target was moving the previous frame or not. A larger value for min_frame_move when stopped will help avoid overlapping conditions and require a greater effort for the camera to move when starting to move.

You may also want to base the look_ahead_distance on the camera distance and (tangent of half of the) horizontal field of view, so the target will offset equally in screen space rather than world space and risk being out of view.

There are many variations to horizontal camera positioning and I encourage a lot of exploration. I have previously posted about fitting a target and look ahead offset into a cylinder that can help place the camera.

Let’s go back to the over the shoulder camera. We need to add another constraint to help find the horizontal camera positioning because keeping the target centered with an over the shoulder camera is not great (although I did that for Transformers: Revenge of the Fallen for a reason)

We want the target to be centered at ¼ of left side of the screen from the center, leaving the center of the screen for the environment and gameplay. This value might not work out but it is a good start.

We can compose a plane to match up with the same way we found a vertical plane to match up with by using the horizontal field of view (assuming side is to the right):

tan(vertical plane angle) = ¼ tan(fovhorizontal/2)
nhorizontal= cos(vertical plane angle) * side + sin(vertical plane angle) * forward

Combining the horizontal and vertical camera positioning

So let’s combine the constraints. For both cameras we have one vertical and one horizontal plane and a single point that should line up with those planes. Refer to planar intersections in this post which results in a line, and fortunately we have a known point on the intersection line and can rely on just the cross product of the plane normals:

camera position = camera distance * (nhorizontalnvertical) + target reference point

Where target reference point is the feet (plus look ahead) or the shoulder center. Depending on the handedness you may need to swap the order of the normals in the cross product.

You could arrive at a similar solution by experimenting with angle offsets from the camera direction, but I prefer to start at where things should fit on screen and work from there and that is why it makes sense to reason about placement with planes and using the camera field of view.

As far as next steps goes there are lots of things to explore. Try using the closest lower ground position (within reason) when jumping instead of the in-air feet position and see how it feels. For the over the shoulder camera, imagine that there is a gun or similar pointing at the center of the screen and try to improve the camera controls to make it feel like aiming.

No comments:

Post a Comment