Expanding on my comment, the basic approach for a single zoom-step would be:
- Project object on screen and remember coordinates
- Reduce fov
- Unproject remembered screen coords (raycast into view space)
- Rotate camera to align ray with the camera-to-object vector (in view space)
Step 4
When unprojecting the point using the inverse projection matrix, the ray will already be in view space (camera space) by default. To get the camera-to-object vector (let's call it toObj
) in view space, it's easiest to transform the object position using the VIEW * MODEL
transform. The camera is at [0, 0, 0]
in view space, so that transformed vector is toObj
.
The following assumes the Y-axis is up/down in view space. This may or may not be the case in your engine; verify this.
Get rotation that aligns ray with toObj
:
ray = normalize(ray)
toObj = normalize(toObj)
angle = acos(dotProduct(ray, toObj)) // NOTE: may need to negate this
if (angle < THRESHOLD) exit // THRESHOLD is a small value, like 0.001
axis = normalize(crossProduct(ray, toObj))
rotationRay = { angle, axis }
This would be the rotation to rotate the camera with, but in many cases, we'd get a funky orientation because the camera will be rolling as well (rotating up vector around forward axis).
To fix this, we first determine the rotated up-vector to get the right-vector when considering toObj
as the forward vector. Then we determine the desired right-vector and rotate around the toObj
axis to align them (Using the right-vectors is just to save an extra calculation that would occur to get the desired up-vector):
upRotated = rotate(UP_VECTOR, rotationRay) // UP_VECTOR is (0, 1, 0)
right = normalize(crossProduct(upRotated, toObj))
rightDesired = crossProduct(UP_VECTOR, toObj)
angle = acos(dotProduct(right, rightDesired)) // NOTE: may need to negate this
rotationRoll = { angle, ray }
The last statement rotates around the ray
axis, because we will do this transform after rotating the camera with rotationRay
, so from the camera's point-of-view, ray
will remain unchanged.
Now we have to combine these rotations and transform the combined rotation back to world space (since your camera transform is probably defined in world space).
// combine rotations with quaternion product
q = quaternion(rotationRoll) * quaternion(rotationRay)
rotationView = q.toAngleAxis()
// transform rotation to world space
axis = inverse(VIEW).transformNormal(rotationView.axis)
rotationWorld = { rotationView.angle, axis }
I've used a fictional transformNormal
method to emphasize that the transform disregards translation and should strictly also be done with the transpose of the inverse of the matrix in case of scaling, but that probably doesn't occur.
Now you have the camera's rotation to perform in world space. If the camera already has an orientation, you can combine both rotations using the quaternion product like demonstrated earlier.
Keep in mind this was all written without testing or sketching anything, so maybe I messed something up along the way. Regardless, I hope it points you in the right direction. Good luck :)