2

I would like to modify the FOV value of my camera during an animation. Unfortunately as soon as I implement the FOV value, I can observe my scene becoming smaller.

So I've been wondering, what's the mathematic link between FOV value and the distance position of a perspective camera ?

The idea is to have the same scene (same size by modifying the camera position) while the FOV value is changing.

Thank you very much.

EDIT 1 :

Here's a snippet that illustrates my issue : When I implement the FOV value of my camera (from 4 to 45), the distance between my square and my camera changes. How can I prevent it ?

        let W = window.innerWidth, H = window.innerHeight;
        
        let renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true } );
        renderer.setPixelRatio( window.devicePixelRatio );
        renderer.setSize( W, H );

        document.body.appendChild( renderer.domElement );

        let camera = new THREE.PerspectiveCamera( 4, W/H, 1, 100 );
        let scene = new THREE.Scene();

        camera.position.set(0,0,14);
        camera.lookAt(0,0,0);

        let geo = new THREE.BoxGeometry(0.5, 0.5, 0.5);
        let mat = new THREE.MeshNormalMaterial();
        let mesh = new THREE.Mesh(geo, mat);
        mesh.rotation.set(0.2,0.4,-0.1);
        scene.add(mesh);

        renderer.render(scene, camera);

        let progress = {};
        progress.fov = 4;

        
            TweenMax.to(progress, 2,{
                fov:45,
                onUpdate:function(){
                    camera.lookAt(0,0,0);
                    camera.updateProjectionMatrix();
                    camera.fov = progress.fov;
                    
                    renderer.render(scene, camera);
                    
                },
                repeat:-1,
                ease:Power3.easeInOut
            });
            
        
body{margin:0;padding:0;overflow:hidden;background: #666;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/2.1.3/TweenMax.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/108/three.js"></script>
gman
  • 100,619
  • 31
  • 269
  • 393
Michaël
  • 489
  • 1
  • 5
  • 21

1 Answers1

8

The perspective projection describes the mapping from 3D points in the world as they are seen from of a pinhole camera, to 2D points of the viewport. This means an object which is projected on the viewport becomes smaller, by its depth.
The relation between the projected area in view space and the Z coordinate of the view space is linear. It depends on the field of view angle and the aspect ratio.
See also THREE.js PerspectiveCamera focalLength off by a factor of two, inconsistent with FOV.

The relation between Z-distance and size for a filed of view fov_y, at perspective projection is:

depht_s = Math.tan(fov_y/2.0 * Math.PI/180.0) * 2.0;

The projected size of the object on the viewport depends on the filed of view and the depth. This cause that a 3 dimensional object never can "look" the same, when filed of view changes. You can define a plane in a certain distance (depth) and find a new distance, so that the projection of the object doesn't change in size, for exactly that depth. Of course the projected size of the object "before" and "behind" this certain distance will (at least slightly) change. See also How to switch between Perspective and Orthographic cameras keeping size of desired object respectively Transpose z-position from perspective to orthographic camera in three.js .

See the illustration. While the top point of the cube can't be seen from the camera when the object is near to the camera, it can be seen when it is far away:

The initial filed of view is 4.0. So the ration between Z-distance and size is:

let init_depht_s    = Math.tan(4.0/2.0 * Math.PI/180.0) * 2.0;

When the filed of view is animated, then the current ration between Z-distance and size is:

let current_depht_s = Math.tan(progress.fov/2.0 * Math.PI/180.0) * 2.0;

Now you've to define a distance, which has to "keep size". The initial distance to the center of the cube is 14.0, so I'll choose this for the reference distance. This distance has to be scaled by the ration init_depht_s / current_depht_s, then the projection of the cube (exactly at this distance) keeps its size:

camera.position.set(0, 0, 14 * init_depht_s / current_depht_s);

See the example, which is based on your original code (I've change the near plane to 0.1, else the cube would be clipped, because final distance is below 1.0):

let W = window.innerWidth, H = window.innerHeight;
        
let renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( W, H );

document.body.appendChild( renderer.domElement );

let camera = new THREE.PerspectiveCamera( 4, W/H, 0.1, 100 );
let scene = new THREE.Scene();

camera.position.set(0,0,14);
camera.lookAt(0,0,0);

let geo = new THREE.BoxGeometry(0.5, 0.5, 0.5);
let mat = new THREE.MeshNormalMaterial();
let mesh = new THREE.Mesh(geo, mat);
mesh.rotation.set(0.2,0.4,-0.1);
scene.add(mesh);

renderer.render(scene, camera);

let progress = {};
progress.fov = 4;

TweenMax.to(progress, 2,{
    fov:45,
    onUpdate:function(){
        let init_depht_s    = Math.tan(4.0/2.0 * Math.PI/180.0) * 2.0;
        let current_depht_s = Math.tan(progress.fov/2.0 * Math.PI/180.0) * 2.0;

        camera.position.set(0, 0, 14 * init_depht_s / current_depht_s);
        camera.lookAt(0,0,0);
        camera.updateProjectionMatrix();
        camera.fov = progress.fov;
        
        renderer.render(scene, camera);
        
    },
    repeat:-1,
    ease:Power3.easeInOut
});
body{margin:0;padding:0;overflow:hidden;background: #666;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/2.1.3/TweenMax.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/108/three.js"></script>
Rabbid76
  • 202,892
  • 27
  • 131
  • 174