3D zooming technique to maintain the relative position of an object on screen

Is it possible to zoom to a certain point on screen by modifying the field of view and rotating the view of the camera as to keep that point/object in the same place on screen while zooming ? Changing the camera position is not allowed.

I projected the 3D pos of the object on screen and remembered it.

Then on each frame I calculate the direction to it in camera space and then I construct a rotation matrix to align this direction to Z axis (in cam space).

After this, I calculate the direction from the camera to the object in world space and transform this vector with the matrix I obtained earlier and then use this final vector as the camera's new direction.

And it's actually "kinda working", the problem is that it is more/less off than the camera's rotation before starting to zoom depending on the area you are trying to zoom in (larger error on edges/corners).

It looks acceptable, but I'm not settling for only this. Any suggestions/resources for doing this technique perfectly? If some of you want to explain the math in detail, be my guest, I can understand these things well.

-
Isn't this just 1) reduce fov; 2) look at object; 3) repeat; ? – Torious Jun 9 '12 at 21:50
If you can animate it in a 3D editing package, then you can code it. A suggested first step for any "can I do this weird thing" question that both saves a lot of time and lets you preview the effect without having to code it. – Patrick Hughes Jun 9 '12 at 21:53
@Torious I don't want to directly look at the object if it wasn't at the center of the screen to begin with. If the object was initially at, let's say (0.75 0.6) at fov 60 then at fov 50 he should also appear at 2d pos (0.75 0.6).. So I need to find the camera's new direction while keeping in mind the restrictions above.. – stark Jun 9 '12 at 22:01
@Patrick Hughes I don't know if I can animate FOV in 3ds max, but it should look like trying to fake zoom to an object by moving the camera pos along the ray to the object and not modifying the fov (it's 2d pos remains the same). Here I want to do it by modifying the fov/rotation and not by changing the camera's pos :D It looks different you know.. – stark Jun 9 '12 at 22:07
Ah I see, so: 1) project object on screen and remember coords (you have this); 2) reduce fov; 3) unproject remembered screen coords (raycast into world); 4) rotate cam to align ray with vector to object. Is that what you're currently doing? – Torious Jun 9 '12 at 22:44

Expanding on my comment, the basic approach for a single zoom-step would be:

1. Project object on screen and remember coordinates
2. Reduce fov
3. Unproject remembered screen coords (raycast into view space)
4. 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 :)

-
Thank you, I think it will work, your explanation was clear. It will take a few days until I'm able to test this, but after understanding it, I think I know what I was doing wrong :D I'll be back with more details after testing it and I won't forget to accept your answer. Have a nice day.. :D – stark Jun 10 '12 at 11:06

In AS3, the correct and quick solution would be to alter the projection matrix. Beware, culling will stop working correctly and should be disabled. `rx,ry` is zoom-rectangle's center, `rw,rh` width and height.

``````    const viewPort:Rectangle = (this.scene).viewPort;
const viewWidth:Number = viewPort.width;
const viewHeight:Number = viewPort.height;
const rh:Number = rw * (viewHeight/viewWidth); // if we don't use the same aspect ratio, the image will be stretched
const rx:Number = viewPort.x + viewWidth * 0.5 - stageX;
const ry:Number = viewPort.y + viewHeight * 0.5 - stageY;
const scale:Number = viewWidth / rw;

const alterProjectionRaw:Vector.<Number> = origProjection.rawData;
alterProjectionRaw[0] *= scale;
alterProjectionRaw[5] *= scale; // NOTE : if same aspect ratio, viewWidth/rw === viewHeight/rh
alterProjectionRaw[8] = 2*rx/rw;
alterProjectionRaw[9] = -2*ry/rh;
``````
-