Implementing lag for a 3rd person camera

Started by
26 comments, last by Gage64 15 years, 9 months ago
I recently wrote a 3rd person camera for the project I'm working on, and we made extensive use of "smooth values". They're basically small objects that contain a current value, a target value, a smoothing parameter (aka the time constant), and an exponential interpolation function. Every frame you update the smooth values with the delta time, and they interpolate the values smoothly towards their targets. The benefits of these objects are that they're simple, very smooth, relatively fast1, and never overshoot the targets since Ae-kt + B has a horizontal asymptote at 'B'. Each update, the math looks something like this:

float Velocity = (this->Target - this->Current)
Current = Target - Velocity*exp(-DeltaTime/this->SmoothParam)

I use these smooth value objects for pitch and yaw however it should be trivial to adapt for quaternions.

1Instead of using exp() itself, you can get away with the first three or four terms of the Taylor series.
Advertisement
What is the exact meaning of timeDelta? Doesn't it denote the time elapsed since the last recent frame update until this one? If so, is anywhere guaranteed that timeDelta doesn't exceed lag? Due to
t = timeDelta / lag
and no seen restriction of t, I assume that the intentional interpolation may behave accidentally as an extrapolation.

Still under the above assumption, I seems me to be a principle problem to make the "follow me" dependend on the frame rate. For a high framerate, timeDelta would be low, and as a consequence t would be close to 0: the camera wouldn't be rotated much. For a low framerate, timeDelta would be high (as said, perhaps also greater than 1), and the orientation would be closer to the target's one (or behave "chaotic" when exceeding 1).

IMHO, the approaches of Basiror and Drigovas have the advantage to be able to work unrelated to the framerate.

EDIT: This post is not related to Zipster's above, but to Gage64's.

[Edited by - haegarr on July 10, 2008 7:09:09 AM]
Quote:Original post by haegarr
What is the exact meaning of timeDelta? Doesn't it denote the time elapsed since the last recent frame update until this one? If so, is anywhere guaranteed that timeDelta doesn't exceed lag? Due to
t = timeDelta / lag
and no seen restriction of t, I assume that the intentional interpolation may behave accidentally as an extrapolation.


Adding

if (t > 1.0f)    t = 1.0f;

Doesn't make any difference. If I replace timeDelta with a small constant, the rotation feels jittery and the problem doesn't go away.

I seem to remember that slerp always takes the shortest path from one orientation to the other, but if you give it two orientations that are 180 degrees apart, the behavior is undefined. Could this have something to do with this?
Quote:Original post by Gage64
Quote:Original post by haegarr
What is the exact meaning of timeDelta? Doesn't it denote the time elapsed since the last recent frame update until this one? If so, is anywhere guaranteed that timeDelta doesn't exceed lag? Due to
t = timeDelta / lag
and no seen restriction of t, I assume that the intentional interpolation may behave accidentally as an extrapolation.


Adding

if (t > 1.0f)    t = 1.0f;

Doesn't make any difference. If I replace timeDelta with a small constant, the rotation feels jittery and the problem doesn't go away.

I seem to remember that slerp always takes the shortest path from one orientation to the other, but if you give it two orientations that are 180 degrees apart, the behavior is undefined. Could this have something to do with this?


why not just slerp the movement. I used this for a camera in a 3rd camera and it worked great.

percent = magic number which feels right in-gamecurrent = current camera positiontarget = character position + rigid camera offseteffective = slerp (target, current, percent * time-delta)set camera pos to effective


Time delta works to smooth out movement so that a short frame moves less and a longer frame moves more. Its usually in seconds so that figures can be comprehendible before multiplied by time-delta (which may be 0.02 for a 20ms frame).

// this moderates the player's movement so that a short frame moves less (smaller multiplier) and a longer frame moves more to 'catch up'player.pos += player.direction * (player.speed * time_delta)
@thre3dee: I remember seeing something similar (can't remember where), but I wanted to try using quaternions because I thought slerp would make it look smoother. I think I will try this approach now.

Actually in your (pseudo) code, what you are interpolating are positions so shouldn't the interpolation be linear? Does applying slerp to vectors even makes sense?
Quote:Original post by haegarr
What is the exact meaning of timeDelta? Doesn't it denote the time elapsed since the last recent frame update until this one? If so, is anywhere guaranteed that timeDelta doesn't exceed lag? Due to
t = timeDelta / lag
and no seen restriction of t, I assume that the intentional interpolation may behave accidentally as an extrapolation.

There isn't any restriction on 'timeDelta' with respect to 'lag'. The latter is just an arbitrary value used to control the responsiveness of the interpolation (a time constant). It's impossible for the function to extrapolate past the target, since e-t is in the range [0,1] for any non-negative value of 't'. Since 'lag' is strictly positive, and 'timeDelta' is strictly non-negative, that condition is always met.

Quote:Still under the above assumption, I seems me to be a principle problem to make the "follow me" dependend on the frame rate. For a high framerate, timeDelta would be low, and as a consequence t would be close to 0: the camera wouldn't be rotated much. For a low framerate, timeDelta would be high (as said, perhaps also greater than 1), and the orientation would be closer to the target's one (or behave "chaotic" when exceeding 1).

IMHO, the approaches of Basiror and Drigovas have the advantage to be able to work unrelated to the framerate.

A higher framerate produces less interpolation per frame, but that also means you have more frames in a given time period. A low framerate produces more interpolation per frame, but you have less frames in the same given time period. Thus the higher framerate interpolation tends to "catch up" to the lower framerate interpolation, if you were to run high- and low-framerate simulations in parallel (they don't produce exactly the same results, but such is the nature of variable framerate simulation). It doesn't get any more framerate-independent than that. Of course a lower framerate is going to produce jerkier results, but I think we can all agree that a low framerate is just bad in general and no solution is going to be able to avoid the jumps that result from infrequent updates.

Quote:Original post by Gage64
I seem to remember that slerp always takes the shortest path from one orientation to the other, but if you give it two orientations that are 180 degrees apart, the behavior is undefined. Could this have something to do with this?

Yes, I forgot to mention that we also have a special update function for angle values which chooses the shortest path. But that logic, if done properly, is independent of the interpolation logic.

For what it's worth, we've never had any problems with this interpolation logic under variable framerate conditions. Clearly if your framerate is wildly inconsistent then any solution will look bad.
Quote:Original post by Zipster
Quote:Original post by haegarr
What is the exact meaning of timeDelta? Doesn't it denote the time elapsed since the last recent frame update until this one? If so, is anywhere guaranteed that timeDelta doesn't exceed lag? Due to
t = timeDelta / lag
and no seen restriction of t, I assume that the intentional interpolation may behave accidentally as an extrapolation.

There isn't any restriction on 'timeDelta' with respect to 'lag'. The latter is just an arbitrary value used to control the responsiveness of the interpolation (a time constant). It's impossible for the function to extrapolate past the target, since e-t is in the range [0,1] for any non-negative value of 't'. Since 'lag' is strictly positive, and 'timeDelta' is strictly non-negative, that condition is always met.

Quote:Still under the above assumption, I seems me to be a principle problem to make the "follow me" dependend on the frame rate. For a high framerate, timeDelta would be low, and as a consequence t would be close to 0: the camera wouldn't be rotated much. For a low framerate, timeDelta would be high (as said, perhaps also greater than 1), and the orientation would be closer to the target's one (or behave "chaotic" when exceeding 1).

IMHO, the approaches of Basiror and Drigovas have the advantage to be able to work unrelated to the framerate.

A higher framerate produces less interpolation per frame, but that also means you have more frames in a given time period. A low framerate produces more interpolation per frame, but you have less frames in the same given time period. Thus the higher framerate interpolation tends to "catch up" to the lower framerate interpolation, if you were to run high- and low-framerate simulations in parallel (they don't produce exactly the same results, but such is the nature of variable framerate simulation). It doesn't get any more framerate-independent than that. Of course a lower framerate is going to produce jerkier results, but I think we can all agree that a low framerate is just bad in general and no solution is going to be able to avoid the jumps that result from infrequent updates.
Sorry for haven't clarified this earlier in my post above, but my answer wasn't related to your post but Gage64's. And this is also true for the following explanation:

Gage64 used the quotient timeDelta/lag when interpolating orientation as well as position:
q = Quaternion.Slerp(q1, q2, t);camPosition = Vector3.Lerp(camPosition, objPosition, t);
And hence, if t isn't in the range [0,1], the results may differ from what is expected.

From this point, assuming that timeDelta << lag (i.e. a high framerate), t would be close to 0, and the interpolations would return something close to q1 and camPosition. If timeDelta approx. lag, t would be close to 1, and the interpolations would return something close to q2 and objPosition. Now, with timeDelta being framerate related, and lag being a constant, for given orientations and positions in the previous frame and the current frame, the interpolated orientation and position will depend on the framerate! For a correct computation this shouldn't be the case. Regardless how long the compution of the current frame durates, the expected orientation and position is always the same. In theory, the camera is moved along a trajectory build in the past from the motion of the object (compare with Basiror's post), and this should IMHO be done dependend on time elapsed since object motion began, but not dependend on framerate.
Well, I managed to get it working. I stopped using quaternions and slerp and decided to just use vectors and linear interpolation (basically what thre3dee suggested), where I just interpolate the camera's position and point it to the object. I'll probably have to tweak this when I actually start working on the game (right now I'm working on the "engine" portion), but for now it looks pretty good.

For now I consider this as "solved" and will work on some other stuff, but later I'll try to implement the other approaches suggested here to see if they give better results.

For anyone interested, here's the code:

void Update(float timeDelta){    MoveObject(timeDelta);        // The time, in seconds, it takes the camera to "catch up"    // to the object after it stopped moving    const float LAG = 0.25f;        // Calculate the "would be" camera position    Vector3 newCameraPosition = objPosition + Vector3.TransformNormal(cameraOffset, objOrient);        // The vector along which the camera should lag    Vector3 lagVector = newCameraPosition - camPosition;    float length = lagVector.Length();    lagVector.Normalize();        float distance;            // If there's no lag, the object moved a very small distance, or LAG is    // smaller than timeDelta (in which case this will extrapolate), don't interpolate    if (CmpFloats(LAG, 0.0f) || length < 0.01f || LAG < timeDelta)	distance = length;    else	distance = length * timeDelta / LAG;	    camPosition += lagVector * distance;	    // Point the camera to the object    LookAt(objPosition);}    void LookAt(Vector3 target){    Vector3 look = target - camPosition;    look.Normalize();        // Use the object's up vector as the camera's up vector    Vector3 up = new Vector3(objOrient.M21, objOrient.M22, objOrient.M23);    up.Normalize();        Vector3 right = Vector3.Cross(up, look);        // Make sure all 3 vectors are orthogonal    up = Vector3.Cross(look, right);        viewMatrix.M11 = right.X; viewMatrix.M12 = up.X; viewMatrix.M13 = look.X; viewMatrix.M14 = 0;    viewMatrix.M21 = right.Y; viewMatrix.M22 = up.Y; viewMatrix.M23 = look.Y; viewMatrix.M24 = 0;    viewMatrix.M31 = right.Z; viewMatrix.M32 = up.Z; viewMatrix.M33 = look.Z; viewMatrix.M34 = 0;    	    viewMatrix.M41 = -Vector3.Dot(camPosition, right);    viewMatrix.M42 = -Vector3.Dot(camPosition, up);    viewMatrix.M43 = -Vector3.Dot(camPosition, look);    viewMatrix.M44 = 1;    	    // The above code gives the same result as:    // viewMatrix = Vector3.LookAtLH(camPosition, target, up);}


Thanks again to everyone for their help.

This topic is closed to new replies.

Advertisement