calculating size of 3d space unit in pixels on screen

Hi All, thanks in advance for any help anyone can offer.

I’m modifying the rts scroller camera from this post:

[Real-Time Strategy Camera)

I’ve got it setup almost entirely except for one problem. I am scrolling over a 3d mesh (flat plane for now) that is 128X128 and centered on the origin. Scrolling works fine, zooming works fine, but my clamping system needs to be improved. I obviously don’t want to over scroll the mesh, so I calculate the center of my scene and clamp my X and Y camera position to be within the map size, shifted by the distance of my zoom and angle of camera view. This works fine. I am clamping successfully, but only based on the coordinate at the center of my screen. What I really need to do is clamp based when my viewable area will display outside of my ground plane. This number can be thought of my as current clamp coords minus 1/2 of a screen in any direction.

Basically I need to know how to calculate how many units in 3d space == 1/2 a 2d screen, or I need a new approach for clamping to my groundplane.

Here is my code:

from direct.showbase.DirectObject import DirectObject
from pandac.PandaModules import VBase3, Point2, Point3
from math import tan, radians

class gamecam(DirectObject):
    def __init__(self):
        self._gameCam = base.cam
        base.camLens.setFov(40.0)
        base.camLens.setFocalLength(35.0)
        self._gameCam.setPos(0,0,30) 
        
        self.tolerance = 35             # How many pixels from the edge of the window 
        self.cameraMoveDist = 14        # How many units to move the camera 
        self.cameraZBounds = [25, 60]    
        self.cameraXBounds = [-64,64]
        self.cameraYBounds = [-64,64]
        
        #base.disableMouse()
        self._gameCam.lookAt(0,15,0)    # Make the camera look at the floater (creates angle) 
        taskMgr.add(self.handleMouseInput,"mouse_scroll")
        
        self.accept( 'wheel_up', self.zoom, [1] )       # Listen for mouse scrolling 
        self.accept( 'wheel_down', self.zoom, [-1] )    # for zooming 
        
    def handleMouseInput(self, task):  
        # Get the position of the cursor if it is in the window 
        if( base.mouseWatcherNode.hasMouse() ): 
            movementDirection = VBase3(0, 0, 0) # Which direction to move the camera
            md = base.win.getPointer(0) 
            x = md.getX() 
            y = md.getY() 
            
            # If the cursor is on the edges, move the camera 
            if( x + self.tolerance > 640 ): 
                movementDirection.setX(1) 
            elif( x < self.tolerance ): 
                movementDirection.setX(-1) 
            if( y + self.tolerance > 480 ): 
                movementDirection.setY(-1) 
            elif( y < self.tolerance ): 
                movementDirection.setY(1) 
            
            if movementDirection.x != 0 or movementDirection.y !=0:     
                self.moveCamera( movementDirection ) 
            
        return task.cont
                
    def moveCamera(self, dir): 
        deltaPos = dir * self.cameraMoveDist * globalClock.getDt() 
        cpos = self._gameCam.getPos() + deltaPos
        zoomModifiedBound = tan(radians(26.5651)) * self._gameCam.getPos().getZ() #This num is specific to this: self._gameCam.lookAt(0,15,0).
        
        cpos.x = min (max(cpos.x, self.cameraXBounds[0]),self.cameraXBounds[1])
        cpos.y = min (max(cpos.y, self.cameraYBounds[0] - zoomModifiedBound),self.cameraYBounds[1] - zoomModifiedBound)
        
        self._gameCam.setPos( cpos ) 
        
    # Determines the vector from the camera to the floater and moves the 
    # camera closer to or farther from the floater along the vector 
    def zoom(self, dir): 
        camToFloater = VBase3(0, 0.15,0) - VBase3(0,0,0.30)
        camToFloater *= dir
        
        newPos = self._gameCam.getPos() + camToFloater 
        z = newPos.getZ() 
        
        if( z < self.cameraZBounds[1] and z > self.cameraZBounds[0]): 
            self._gameCam.setPos(newPos) 
            
    def setXBounds(self, arr2):
        self.cameraXBounds = arr2
        
    def setYBounds(self, arr2):
        self.cameraYBounds = arr2

Well, self answer that is 75% correct:

from direct.showbase.DirectObject import DirectObject
from pandac.PandaModules import VBase3, Point2, Point3
from math import tan, radians, fabs

class gamecam(DirectObject):
    def __init__(self):
        self._gameCam = base.cam
        self.viewOffset = 15
        
        base.camLens.setFov(40.0)
        base.camLens.setFocalLength(28.0)
        self._gameCam.setPos(0,0,30) 
        
        self.tolerance = 35             # How many pixels from the edge of the window 
        self.cameraMoveDist = 14        # How many units to move the camera 
        
        self.cameraZBounds = [25, 60]    
        self.cameraXBounds = [-64,64]
        self.cameraYBounds = [-64,64]
        
        base.disableMouse()
        self._gameCam.lookAt(0,self.viewOffset,0)    # Make the camera look at the offset (creates angle) 
        taskMgr.add(self.handleMouseInput,"mouse_scroll")
        
        self.accept( 'wheel_up', self.zoom, [1] )       # Listen for mouse scrolling 
        self.accept( 'wheel_down', self.zoom, [-1] )    # for zooming 
        
    def handleMouseInput(self, task):  
        # Get the position of the cursor if it is in the window 
        if( base.mouseWatcherNode.hasMouse() ): 
            movementDirection = VBase3(0, 0, 0) # Which direction to move the camera
            md = base.win.getPointer(0) 
            x = md.getX() 
            y = md.getY() 
            
            # If the cursor is on the edges, move the camera 
            if( x + self.tolerance > 640 ): 
                movementDirection.setX(1) 
            elif( x < self.tolerance ): 
                movementDirection.setX(-1) 
            if( y + self.tolerance > 480 ): 
                movementDirection.setY(-1) 
            elif( y < self.tolerance ): 
                movementDirection.setY(1) 
            
            if movementDirection.x != 0 or movementDirection.y !=0:     
                self.moveCamera( movementDirection ) 
            
        return task.cont
                
    def moveCamera(self, dir): 
        deltaPos = dir * self.cameraMoveDist * globalClock.getDt() 
        cpos = self._gameCam.getPos() + deltaPos
        
        camAngle = 90 - fabs(base.cam.getHpr().getY())
        fov = base.camLens.getFov()
        outOfViewAngleY = camAngle - fov.getY()/2
        outOfViewAngleX = (90-fov.getX())/2
        camZ = self._gameCam.getPos().getZ()
        
        camYToScreenBottom = tan(radians(outOfViewAngleY)) * camZ
        camXToScreenEdge = tan(radians(outOfViewAngleX)) * camZ
        camYToScreenTop = tan(radians(camAngle + outOfViewAngleY)) * camZ
        
        print camYToScreenBottom, camYToScreenTop
        cpos.x = min (max(cpos.x, self.cameraXBounds[0] + camXToScreenEdge),self.cameraXBounds[1] - camXToScreenEdge)
        cpos.y = min (max(cpos.y, self.cameraYBounds[0] - camYToScreenBottom),self.cameraYBounds[1] - camYToScreenTop)
        
        self._gameCam.setPos( cpos ) 
        
    # Determines the vector from the camera to the floater and moves the 
    # camera closer to or farther from the floater along the vector 
    def zoom(self, dir): 
        camToFloater = VBase3(0,self.viewOffset/100,0) - VBase3(0,0,0.3)
        camToFloater *= dir
        
        newPos = self._gameCam.getPos() + camToFloater 
        z = newPos.getZ() 
        
        if( z < self.cameraZBounds[1] and z > self.cameraZBounds[0]): 
            self._gameCam.setPos(newPos) 
            self.moveCamera(VBase3(0,0,0))
            
    def setXBounds(self, arr2):
        self.cameraXBounds = arr2
        
    def setYBounds(self, arr2):
        self.cameraYBounds = arr2

This works for clamping in all directions except the top edge of the screen. I’m not grasping something for the top, because as far as I can tell this is the appropriate way to calculate it, but I still overshoot by a few units.

If anyone has ideas I appreciate it!
Thanks

Finally looked at this again. The fix is:

camYToScreenTop = tan(radians(camAngle + fov.getY()/2)) * camZ