camera orbiting around the point

Amazingly i could not find that kind of snippet anywhere. So i cranked up my rusty brain, remembered little trig i learned at school and made this self-contained snippet:

from direct.showbase.DirectObject import DirectObject
from math import pi, sin, cos

def clamp(val, minVal, maxVal):
    return min(max(val, minVal), maxVal)

class OrbitCameraController(DirectObject):
    def __init__(self, camera, distance=10, distance_limits=(5, 50), name='OrbitCameraController'):
        self.camera = camera
        self.distance = distance
        self.distance_limits = distance_limits
        self._is_rotating = False
        self._rotate_start = (0, 0)
        self._h = self._v = 0
        self._vertical_limits = (-pi / 2 + 0.1, pi / 2 - 0.1)
        self.accept('mouse3', self._setRotating, [True])
        self.accept('mouse3-up', self._setRotating, [False])
        self.accept('wheel_up', self.zoom, [1])
        self.accept('wheel_down', self.zoom, [-1])
        self._camera_task = base.taskMgr.add(self._task, name)
        self._orbitCamera(0, 0)                                             # knock camera into orbit

    def zoom(self, d):
        self.distance = clamp(self.distance + d, *self.distance_limits)
        self._orbitCamera(0, 0)

    def _setRotating(self, rotating=True):
        self._is_rotating = rotating
        base.setMouseHidden(rotating)
        if not rotating:
            base.win.movePointer(0, *self._rotate_start)
        else:
            md = base.win.getPointer(0)
            self._rotate_start = (md.getX(), md.getY())

    def _task(self, task):
        if not self._is_rotating:
            return task.cont

        if not base.mouseWatcherNode.hasMouse():
            return task.cont

        md = base.win.getPointer(0)
        old_x, old_y = self._rotate_start
        dx = md.getX() - old_x
        dy = md.getY() - old_y

        if dx or dy:
            base.win.movePointer(0, *self._rotate_start)
            self._orbitCamera(dx, dy)
        return task.cont

    def _orbitCamera(self, dx, dy):
        distance = self.distance
        h = self._h - dx * 0.01
        v = self._v + dy * 0.01
        v = clamp(v, *self._vertical_limits)
        posZ = sin(v) * distance
        horizontal_plane_dist = cos(v) * distance
        posX = cos(h) * horizontal_plane_dist
        posY = sin(h) * horizontal_plane_dist
        self._h = h
        self._v = v
        self.camera.setPos(posX, posY, posZ)
        self.camera.lookAt(self.camera.getParent())

Camera orbits around it’s parent node. Zooming with mouse wheel works. :slight_smile:

Comments would be appreciated, especially on code in _orbitCamera(). I went most straight-forward way by using vertical (zy plane) and horizontal (xy plane) angles, and solved two triangles to get correct pos of camera. Maybe there is better way?

Here is what I’m currently using for my player camera. It parents an empty node to the target and sets the compass effect on it, and then parents the camera to that; no trig required! I still need to put in distance limits and implement collision with geometry.

class PlayerCamera(DirectObject):
    def __init__(self, target, offset=(0,0,0), distance=50, sensitivity=40.0):
        self.target = target
        self.offset = offset
        self.distance = distance
        self.sensitivity = sensitivity
        base.disableMouse()
        self.slave = base.camera
        self.origin = target.attachNewNode('Camera Orbit Center')
        self.origin.setCompass()
        self.origin.setPos(offset)
    
    def enable(self):
        base.win.requestProperties(HIDE_POINTER)
        base.win.movePointer(0, base.win.getXSize()//2, base.win.getYSize()//2)
        
        self.slave.setHpr(0, 0, 0)
        self.slave.setPos(0, -self.distance, 0)        
        self.slave.reparentTo(self.origin)
        self.slave.lookAt(self.origin)
        
        self.accept('wheel_up',   self.slave.setY, [self.slave, 2])
        self.accept('wheel_down', self.slave.setY, [self.slave, -2])
        self.addTask(self.controltask, "PlayerCameraTask", priority=1)
    
    def disable(self):
        base.win.requestProperties(SHOW_POINTER)
        self.slave.wrtReparentTo(render)
        self.ignoreAll()
        self.removeTask("PlayerCameraTask")
    
    def controltask(self, task):
        win = base.win
        mwn = base.mouseWatcherNode
        if mwn.hasMouse():
            self.origin.setH(self.origin.getH() - mwn.getMouseX() * self.sensitivity)
            self.origin.setP(self.origin.getP() + mwn.getMouseY() * self.sensitivity)
            win.movePointer(0, win.getXSize()//2, win.getYSize()//2)
        return task.cont

thats really neat! i wasnt ware of setCompass(), thanks :slight_smile: