Simple zoom-in / binoculars

I’ve hacked together a simple zoom-in functionality for my game engine. It isn’t true zooming, but it gives the impression of using binoculars by setting the field of view to a small value.

First I define a global variable zoomed and a simple function to adjust the FOV:

zoomed = 0

def fovZoom(t):
        base.camLens.setFov(t)

And then I attach a method to the zoom key. The following includes pseudo code - I don’t particularly want to include the key handling function here, as it isn’t my work.

if <zoom key is down>:
            if not zoomed:
                zoomed = 1
                fovZoomer = LerpFunc(self.fovZoom, 0.1, 45, 13, 'easeOut', [], "zoomer")
                fovZoomer.start()
        else:
            if zoomed:
                zoomed = 0
                fovZoomer = LerpFunc(self.fovZoom, 0.1, 13, 45, 'easeIn', [], "zoomer")
                fovZoomer.start()

It eases into and out of the ‘binoculars’, which adds a nice touch. A shader, or 2D texture overlay (GUI style card that covers the whole screen) could be used to darken/dim the edges of the screen, adding to the ‘tunnel vision’ effect.

That sounds exactly like true zooming to me.

David

Thanks David, perhaps it is :confused:

From memory I read an academic OpenGL paper ages ago that described what they called ‘true’ zooming. I think it had something to do with adding more detail the closer you zoom in (sounds like LOD?), or something like that.

Anyway, it works for me and hopefully it may save someone else a few minutes of time later on… :smiley:

the zoom you did is as true of a zoom as you can get without adding depth of field, lens flairs and other next gen stuff.

Thanks treeform and David,

I have a related question now, for anyone who has played half life 2.
In the opening scene, the player sees gman talking for a bit, then the camera fades into the train, but at the same time there is some sort of zoom/stretch function happening on the screen. -I’ve seen the same effect in ?horror? scenes at the movies I think, just can’t remember it’s name.

Does anyone know how this effect is done? My first impression is that the FOV is lerp’d from a wide, fisheye sort of value to the ordinary value (70 in half life?), but at the same time, the camera’s location is moved backwards faster than the impression given by the lerp function. In HL2, this, combined with the narrow train + the movement of the train gives the ‘tunnel vision’ zoom effect (i think - not really sure though).

Hopefully someone can understand that, I’ll do up a demo later and post the code to see what others think, and to see if my idea about it was accurate.

I believe what you are looking for is the dolly zoom (also affectionately known as the zolly). The way you describe it is pretty much how it is achieved, a change in zoom and camera movement forward/backward.

Thanks again ZeroByte! That’s exactly what I meant.
I’ll throw together a piece of code showing that, and the zoom function this afternoon and post it back here.

ps - on further inspection, I think the HL intro does actually use it, only very slightly

Here’s a tute showing how to do both dolly zoom and the binocular effect:

# Panda3D dolly zoom and binoculars example [aurilliance 3/3/09]
# This code just shows how to create a 'dolly zoom' effect
# such as the one seen in the opening scene of half-life 2.
# This effect is achieved by simultanously decreasing the 
# field of view (FOV) and moving the camera forwards slightly.
#
# It also shows how to use Lerp intervals to create a nice, 
# smooth binoculars / weapon scope effect that zooms in on
# what the player is looking at.

import direct.directbase.DirectStart
from pandac.PandaModules import *
from direct.interval.IntervalGlobal import *
from direct.showbase.DirectObject import DirectObject
from direct.gui.OnscreenText import OnscreenText
from direct.task.Task import Task

import sys

# Function to print text to the screen
def genLabelText(text, i):
    return OnscreenText(text = text, pos = (-1.3, .95-.05*i), fg=(1,1,1,1), align = TextNode.ALeft, scale = .05, mayChange=1)

class World(DirectObject):
    def __init__(self):
        # Game state variables and window setup
        self.zoomed = 0
        self.controls_disabled = 1
        self.cam_height = 1.0
        
        p = WindowProperties()
        p.setCursorHidden(True)
        p.setSize(800, 600)
        base.win.requestProperties(p)
        
        # Set the FOV to a wide value and the camera
        # slightly forwards (to show the dolly zoom)
        base.camLens.setFov(120)
        base.camera.setPos(Point3(0,20,self.cam_height))
        
        # Add some text to the screen
        genLabelText("Panda3D Dolly Zoom and Binoculars example", 0)
        genLabelText("WASD + mouse to look and move, F to use binoculars", 1)
        genLabelText("Controls are enabled when the dolly zoom finishes", 2)
        
        # Set up control
        self.keyMap = {"left":0, "right":0, "up":0, "down":0, "zoom":0}
        self.accept("escape", sys.exit)
        self.accept("a", self.setKey, ["left",1])
        self.accept("d", self.setKey, ["right",1])
        self.accept("w", self.setKey, ["up",1])
        self.accept("s", self.setKey, ["down",1])
        self.accept("f", self.setKey, ["zoom",1])
        self.accept("a-up", self.setKey, ["left",0])
        self.accept("d-up", self.setKey, ["right",0])
        self.accept("w-up", self.setKey, ["up",0])
        self.accept("s-up", self.setKey, ["down",0])
        self.accept("f-up", self.setKey, ["zoom",0])
        base.disableMouse()
        
        # Load a few models to show off the effect
        self.box = loader.loadModel("box")
        self.box.reparentTo(render)
        self.box.setPos(-2,23,0)
        
        self.smiley = loader.loadModel("smiley")
        self.smiley.reparentTo(render)
        self.smiley.setPos(2,23,1)
        
        self.teapot = loader.loadModel("teapot")
        self.teapot.reparentTo(render)
        self.teapot.setPos(0,30,0)
        
        # Add a point light to light things up a bit
        plight = PointLight('plight')
        plight.setColor(VBase4(1,1,1,1))
        plnp = render.attachNewNode(plight)
        plnp.setPos(0,0,10)
        render.setLight(plnp)
        render.setShaderAuto()
        
        # Create and play the dolly zoom effect
        # It is set to zoom from FOV 120 to 45, moving the camera 5
        # units in 10 seconds. All these parameters can be adjusted
        # to create different effects / to suit your taste
        dollyZoomer = LerpFunc(self.fovSet, 10, 120, 45, 'easeOut')
        camMover = LerpPosInterval(base.camera, 10, Point3(0,15,self.cam_height), Point3(0,20,self.cam_height), blendType='easeOut')
        dollySequence = Sequence(Wait(3), Parallel(dollyZoomer, camMover), Func(self.setControls, 0))
        dollySequence.start()
        
        # Add the frame task to task list
        taskMgr.add(self.frame, "moveFunc")
    
    def setKey(self, key, value):
        self.keyMap[key] = value
    
    # Sets whether the keys and mouse are disabled or not
    def setControls(self, value):
        self.controls_disabled = value
    
    # This function is used (generally by a lerp function) to adjust the FOV
    def fovSet(self, t):
        base.camLens.setFov(t)
    
    # The frame task
    def frame(self, task):
        cam_height = 1.0
        cam_sensitivity = 0.1
        speed = 0.35
        strafe_speed = 0.2
        
        # Mouse Control
        md = base.win.getPointer(0)
        x = md.getX()
        y = md.getY()
        
        rotx, roty = 0, 0
        if base.win.movePointer(0, 400, 300):
            rotx -= (x - 400)*cam_sensitivity
            roty -= (y - 300)*cam_sensitivity
        if (roty < -80): roty = -80
        if (roty >  80): roty =  80
        
        # Exit here if the controls are still disabled,
        # we let the above code execute to prevent the player
        # moving the mouse before the dolly zoom finishes
        if self.controls_disabled:
            return Task.cont
        
        base.camera.setHpr(base.camera.getH()+rotx, base.camera.getP()+roty, 0)
        forward_dir = base.camera.getNetTransform().getMat().getRow3(1)
        strafe_dir = base.camera.getNetTransform().getMat().getRow3(0)
        forward_dir.normalize(); strafe_dir.normalize()
        forward_dir *= speed; strafe_dir *= strafe_speed
        
        # Key Movement
        if self.keyMap["up"]:
            base.camera.setPos(base.camera.getPos()+forward_dir)
        if self.keyMap["down"]:
            base.camera.setPos(base.camera.getPos()-forward_dir)
        if self.keyMap["left"]:
            base.camera.setPos(base.camera.getPos()-strafe_dir)
        if self.keyMap["right"]:
            base.camera.setPos(base.camera.getPos()+strafe_dir)
        
        # This handles the binoculars
        # It quickly lerps from FOV 45 (assumed to be the initial setting)
        # to 13 - the smaller the final value, the more it is 'zoomed in'
        if self.keyMap["zoom"]:
            if not self.zoomed:
                self.zoomed=1
                fovZoomer = LerpFunc(self.fovSet, 0.1, 45, 13, 'easeOut', [], "zoomer")
                fovZoomer.start()
        else:
            if self.zoomed:
                self.zoomed=0
                fovZoomer = LerpFunc(self.fovSet, 0.1, 13, 45, 'easeIn', [], "zoomer")
                fovZoomer.start()
        
        return Task.cont

w = World()
run()