# Copyright (C) 2012 Mortal Coil Games # See LICENSE for details. """ Basic CameraController @author: judd """ import sys from zope.interface import implements from mv3d.util.math3d import Vector, Quaternion from mv3d.util.input import MouseEvent, KeyEvent from mv3d.client.ui.irenderer import ISceneNode, ICamera from mv3d.util.icamera import ICameraController, RelativeTo class PandaRay(object): def __init__(self, viewportRay, camera): self.origin = viewportRay.getOrigin() self.origin = Vector(-self.origin[0], self.origin[2], self.origin[1]) + camera.position self.direction = Vector(viewportRay.getDirection()).normalize() self.direction = Vector(-self.direction[0], self.direction[2], self.direction[1]) self.direction = camera.orientation.rotate(self.direction) def getDirection(self): return self.direction def getOrigin(self): return self.origin def getPoint(self, distance): return self.origin + self.direction * distance class CameraController(object): """ Pluggable camera controls for Guide RenderFrames """ implements(ICameraController) name = "Cam" renderer = None guideWidget = None renderWindow = None registerUpdateCallback = True paused = False _setupDone = False _node = None _cam = None def __init__(self): pass def __str__(self): """ Convert to a string """ return self.__class__.__name__ def setup(self, guideWidget, renderer, renderWindow, camera=None, node=None): """ setup """ if self._setupDone: return self.renderer = renderer self.renderWindow = renderWindow self.guideWidget = guideWidget if camera is None: self._cam = renderer.getRendererClass(ICamera)(renderer) self._cam.initialize("Camera-%s" % self.name, renderWindow) else: self._cam = camera if node is None: parent = None if guideWidget is not None: parent = guideWidget.sceneNode self._node = renderer.getRendererClass(ISceneNode)(renderer, parent) self._node.initialize("CameraNode-%s" % self.name) self._node.addObject(self._cam) else: self._node = node if renderer.camera is None: self.renderer.camera = self self.renderer.addKeyListener(self._onKeyEvent) self.renderer.addMouseListener(self._onMouseEvent) if self.registerUpdateCallback: self.renderer.addFrameCallback(self.update) if self.guideWidget is not None: self.guideWidget.setupCamera(self) self.postSetup() # let derived classes do stuff after setup self._setupDone = True def destroy(self): """ Destroy the camera. """ self._node.finalize(True) self._cam.finalize() self.renderer.remFrameCallback(self.update) self.renderer.removeKeyListener(self._onKeyEvent) self.renderer.removeMouseListener(self._onMouseEvent) def pause(self): """ Stop the camera from updating """ if self.paused: return if self.registerUpdateCallback: self.renderer.remFrameCallback(self.update) self.renderer.removeKeyListener(self._onKeyEvent) self.renderer.removeMouseListener(self._onMouseEvent) self.paused = True def resume(self): """ Resume updating after pausing """ if not self.paused: return self.renderer.addFrameCallback(self.update) self.renderer.addKeyListener(self._onKeyEvent) self.renderer.addMouseListener(self._onMouseEvent) if self.guideWidget is not None: self.guideWidget.setupCamera(self) self.paused = False self.renderer.camera = self def _onKeyEvent(self, event): """ Handle keyboard input """ def _onMouseEvent(self, event): """ Handle mouse input """ def postSetup(self): """ Derived classes can override this to do stuff after setup() """ def getRealCamera(self): """ getRealCamera """ return self._cam.camera def getRealCameraNode(self): """ getRealCameraNode """ return self._node def update(self, timeSinceLastFrame): """ update """ def lookAt(self, vec, relativeTo=RelativeTo.local, localDirVec=None): """ lookAt """ self._node.lookAt(vec, relativeTo, localDirVec) def translate(self, offset, relativeTo=RelativeTo.local): """ translate """ self._node.translate(self._cam.getOrientation().rotate(offset), relativeTo) def pitch(self, angle, relativeTo=RelativeTo.local): """ pitch """ self._cam.pitch(angle, relativeTo) def yaw(self, angle, relativeTo=RelativeTo.local): """ yaw """ self._cam.yaw(angle, relativeTo) def roll(self, angle, relativeTo=RelativeTo.local): """ roll """ self._cam.roll(angle, relativeTo) def getCameraToViewportRay(self, pos, returnOffsetAndDirection=False): """ Gets a world space ray as cast from the camera through a viewport position. """ ray = self._cam.getRay(pos) if returnOffsetAndDirection: return (Vector(ray.getOrigin()), Vector(ray.getDirection())) return ray def getPosition(self): """ getPosition """ return self._node.getPosition() def setPosition(self, pos): """ setPosition """ self._node.setPosition(pos) position = property(getPosition, setPosition) def getOrientation(self): """ getOrientation """ return Quaternion(self._node.getOrientation()) * self._cam.orientation def setOrientation(self, ori): """ setOrientation """ self._node.setOrientation(ori) orientation = property(getOrientation, setOrientation) def getNearClip(self): """ getNearClip """ return self._cam.getNearClip() def setNearClip(self, dist): """ setNearClip """ self._cam.setNearClip(dist) nearClip = property(getNearClip, setNearClip) def getFarClip(self): """ getFarClip """ return self._cam.getFarClip() def setFarClip(self, dist): """ setFarClip """ self._cam.setFarClip(dist) farClip = property(getFarClip, setFarClip) def getFov(self): """ getFov """ return self._cam.getFov() def setFov(self, angle): """ setFov """ self._cam.setFov(angle) fov = property(getFov, setFov) def getAspectRatio(self): """ getAspectRatio """ return self._cam.getAspectRatio() def setAspectRatio(self, ratio): """ setAspectRatio """ self._cam.setAspectRatio(ratio) aspectRatio = property(getAspectRatio, setAspectRatio) def getAutoAspectRatio(self): """ getAspectRatio """ return self._cam.getAutoAspectRatio() def setAutoAspectRatio(self, auto): """ setAspectRatio """ self._cam.setAutoAspectRatio(auto) autoAspectRatio = property(getAutoAspectRatio, setAutoAspectRatio) def pickObject(self, position): """ Casts a ray at position and returns the object underneath or none """ if self.guideWidget is None: return None return self.guideWidget.mousePick(position) def getViewportRay(self, position): """ Returns (origin, normalized direction vector) for the given 2d position on the screen. """ # TODO: This is confusing, why this and getCameraToViewportRay? if self.renderWindow is None: return if self.guideWidget is None: return size = self.guideWidget.window.GetClientSize() ray = self.getCameraToViewportRay((position[0] / float(size[0]), position[1] / float(size[1]))) return ray def getViewpointLocation(self, position): """ the x and y component are the 2d space, and the z is how far away from the screen """ if self.renderWindow is None: return if self.guideWidget is None: return size = self.guideWidget.Size ray = self.getCameraToViewportRay((position[0] / float(size[0]), position[1] / float(size[1]))) return Vector(ray.getPoint(position[2])) def saveScreenshot(self, filename): """ Save a screenshot to the given filename """ self._cam.saveScreenshot(filename) class InputCameraController(CameraController): """ A CameraController that also handles input events """ name = "InputCam" lastMouseEvt = None def setup(self, guideWidget, renderer, renderWindow, camera=None, node=None): CameraController.setup(self, guideWidget, renderer, renderWindow, camera, node) renderer.addMouseListener(self._onMouseEvent) renderer.addKeyListener(self._onKeyEvent) def _onMouseEvent(self, mv3dMouseEvt): """ Handles panda mouse events and passes data to mouseEvent. The renderer passes us premade mv3d MouseEvent objects, so just pass em right through. """ if self.paused: return if self.guideWidget is not None and "win" in sys.platform: if self.guideWidget.window.FindFocus() != self.guideWidget.window: return # if self.renderer.getType() == Panda3D: # self._cam._displayRegion.setActive(True) self.mouseEvent(mv3dMouseEvt) self.lastMouseEvt = mv3dMouseEvt def _onKeyEvent(self, mv3dKeyEvt): """ Handles panda key events and passes data to keyEvent. The renderer passes us premade mv3d KeyEvent objects, so just pass em right through. """ if self.paused: return if self.guideWidget is not None: if self.guideWidget.window.FindFocus() != self.guideWidget.window: return self.keyEvent(mv3dKeyEvt) def _wxInputEvent(self, wxEvt, wx, wxKeyMap): """ Handles wxWidgets events and passes data to mouseEvent and keyEvent """ if self.paused: wxEvt.Skip() return if wx is None: raise RuntimeError("Please install wx libraries!") if isinstance(wxEvt, wx.MouseEvent): evt = MouseEvent.fromWxEvent(wxEvt, self.lastMouseEvt, wx) self.mouseEvent(evt) self.lastMouseEvt = evt elif isinstance(wxEvt, wx.KeyEvent): evt = KeyEvent.fromWxEvent(wxEvt, wxKeyMap, wx) self.keyEvent(evt) wxEvt.Skip() def mouseEvent(self, evt): """ Override this method to handle mouse events """ def keyEvent(self, evt): """ Override this method to handle key events """ class FreelookCameraController(InputCameraController): """ Camera controller for flying around """ name = "FreelookCam" def __init__(self): InputCameraController.__init__(self) self.rot = [0, 0] self.trans = Vector(0, 0, 0) self.moveScale = 1 self.lastMouseEvt = None self.dragging = False self.moving = False def postSetup(self): """ Default settings for this camera; tools can and probably should override this method. """ self.nearClip = 0.1 self.farClip = 2000 def onDrag(self, evt): """ Callback for when the mouse is dragged """ def onStopDrag(self, evt): """ Callback for when the mouse stops dragging """ def mouseEvent(self, evt): """ mouseEvent """ self.rot = [0, 0] if evt.wheelRotation: self.translate((0, 0, evt.wheelRotation * self.moveScale / -10.0)) return True # turn the camera if the right mouse button is held if evt.rightIsDown: self.rot[0] = -evt.rel[0] * 0.05 self.rot[1] = -evt.rel[1] * 0.05 self.pitch(self.rot[1]) self.yaw(self.rot[0], relativeTo=RelativeTo.world) return True if evt.middleIsDown: trans = (Vector(evt.rel[0], evt.rel[1], 0) * self.moveScale) self.translate(trans) return True if not evt.leftIsDown and not evt.rightIsDown: if self.dragging: self.dragging = False self.onStopDrag(evt) elif evt.leftIsDown and self.lastMouseEvt is not None and self.lastMouseEvt.leftIsDown: self.dragging = True self.onDrag(evt) return True def keyEvent(self, evt): """ Handle key events """ assert isinstance(evt, KeyEvent) if evt.mods is not None and evt.mods['shift']: speed = self.moveScale * 0.25 else: speed = self.moveScale char = evt.text.lower() if evt.keyDown: if char == 'w': self.trans.z = -speed elif char == 's': self.trans.z = speed if char == 'd': self.trans.x = speed elif char == 'a': self.trans.x = -speed if char == 'x': self.trans.y = -speed elif char == ' ': self.trans.y = speed else: if char == 'w': self.trans.z = 0 elif char == 's': self.trans.z = 0 if char == 'd': self.trans.x = 0 elif char == 'a': self.trans.x = 0 if char == 'x': self.trans.y = 0 elif char == ' ': self.trans.y = 0 if self.trans == Vector.zero(): self.moving = False else: self.moving = True def update(self, _timeSinceLastFrame): """ frameEnded callback """ if self.moving: self.translate(self.trans, relativeTo=RelativeTo.local) class OrbitCameraController(InputCameraController): """ Camera controller for orbiting around an object """ name = "OrbitCam" targetNode = None targetRadius = 0.0 orbitDist = 10.0 minDist = 1.0 maxDist = None dragging = False moving = False moveScale = 5 orbitSpeed = 0.05 allowTrans = True lookAtRelative = RelativeTo.parent def __init__(self): InputCameraController.__init__(self) self.rot = [0, 0] self.trans = Vector(0, 0, 0) self._target = Vector() def setTarget(self, target): """ Sets the target the camera will orbit around. This can be a vector or vector-like object. It can also be a scenenode in the appropriate renderer. If set to a node, the camera will always stay relative to that node. """ try: self._target = Vector(target) except: self._target = target def getTarget(self): """ Gets the position of the target. """ if isinstance(self._target, Vector): return self._target nodeClass = self.renderer.getRendererClass(ISceneNode) if isinstance(self._target, nodeClass): self.targetNode = self._target return Vector(self._target.position) # if all else fails, check if the thing has a position attr and try that if hasattr(self._target, "position"): return Vector(self._target.position) raise TypeError("Can't get a vector from '%s'" % str(self._target)) target = property(getTarget, setTarget) def setTargetRadius(self, radius=None): """ Sets the size of the target (which will offset the orbit distance.) If no radius is specified, try and figure one out based on the scenenode we should have at self._node. If all else fails, the radius is set to 0. """ if radius is not None: self.targetRadius = radius else: if self.targetNode is not None: self.targetRadius = self.targetNode.getRadius() else: self.targetRadius = 0.0 self.orbitDist = self.targetRadius * 2 + 10 self.moveScale = self.targetRadius / 20.0 + 2.5 def resetOrbit(self): """ Reorient the camera to the default position. Handy if the target changes. """ self.orbitDist = self.targetRadius * 2 + 10 self.rot = [0, 0] self.trans = Vector() def update(self, _timeSinceLastFrame): """ Called every frame. Sets the position and orientation of the camera. This is done every frame since if the target is a node, the camera will automatically follow it. """ if self.renderWindow is None: return offset = Quaternion.fromEuler(self.rot[0], 0, 0) * Vector(0, 0, - self.orbitDist) offset = Quaternion.fromEuler(0, self.rot[1], 0) * offset target = self.target self.position = offset + target + self.trans self.lookAt(offset, relativeTo=RelativeTo.local, localDirVec=(0, 0, -1)) def mouseEvent(self, evt): """ Handle a mouse event. Mouse wheel changes orbit dist, right drag rotates, middle drag translates the target position. Returns True to indicate that further processing of this event should be stopped. """ if evt.wheelRotation: self.orbitDist -= evt.wheelRotation / evt.wheelDelta * self.moveScale * (self.orbitDist / 100.0) if self.minDist is not None: self.orbitDist = max(self.orbitDist, self.minDist) if self.maxDist is not None: self.orbitDist = min(self.orbitDist, self.maxDist) return True # rotate the camera if the right mouse button is held if evt.rightIsDown: self.rot[0] += evt.rel[1] * self.orbitSpeed self.rot[1] += -evt.rel[0] * self.orbitSpeed self.rot[0] = min(max(self.rot[0], -1.57), 1.57) while self.rot[1] > 6.282: self.rot[1] -= 6.282 while self.rot[1] < 0: self.rot[1] += 6.282 return True elif evt.middleIsDown and self.allowTrans: trans = (Quaternion(self.orientation) * Vector(evt.rel[0], evt.rel[1], 0) * self.moveScale * (self.orbitDist / 100.0)) self.trans += trans return True return False class ChaseCameraController(InputCameraController): """ Camera controller for orbiting around an object while chasing it """ name = "ChaseCam" targetNode = None targetRadius = 0.0 orbitDist = 10.0 minDist = 5.0 maxDist = None dragging = False moving = False moveScale = 1 orbitSpeed = 0.05 allowTrans = True registerUpdateCallback = False def __init__(self): InputCameraController.__init__(self) self.rot = [0, 0] self.trans = Vector(0, 0, 0) self.target = None def postSetup(self): """ Default settings for this camera; tools can and probably should override this method. """ self.nearClip = 0.1 self.farClip = 2000 def resetOrbit(self): """ Reorient the camera to the default position. Handy if the target changes. """ self.orbitDist = 25 self.rot = [-0.5, 0] self.trans = Vector() def update(self, _timeSinceLastFrame): """ Called every frame. Sets the position and orientation of the camera. This is done every frame since if the target is a node, the camera will automatically follow it. """ if self.renderWindow is None or self.target is None or self.paused: return offset = Quaternion.fromEuler(self.rot[0], 0, 0) * Vector(0, 0, - self.orbitDist) offset = Quaternion.fromEuler(0, self.rot[1] + 3.14, 0) * offset offset = Quaternion(self.target.getOrientation()) * offset self.position = offset + self.target.getPosition() + self.trans # cast a ray to make sure we aren't going through stuff if hasattr(self.target, "area"): space = self.target.area.getDisabledSpace() ray = space.castRay( self.target.getPosition(), self.position, ignoreColliders=[self.target.bodycollider.tgeom]) if ray.mingeom is not None: delta = Vector(self.position) - self.target.getPosition() delta = delta.normalize() self.position = delta * -2 + ray.minpos self.lookAt(offset, RelativeTo.local, localDirVec=(0, 0, -1)) def mouseEvent(self, evt): """ Handle a mouse event. Mouse wheel changes orbit dist, right drag rotates, middle drag translates the target position. Returns True to indicate that further processing of this event should be stopped. """ if evt.wheelDelta: self.orbitDist -= evt.wheelRotation / evt.wheelDelta * self.moveScale * 2 if self.minDist is not None: self.orbitDist = max(self.orbitDist, self.minDist) if self.maxDist is not None: self.orbitDist = min(self.orbitDist, self.maxDist) return True # rotate the camera if the right mouse button is held if evt.rightIsDown: self.rot[0] += evt.rel[1] * self.orbitSpeed self.rot[1] += -evt.rel[0] * self.orbitSpeed self.rot[0] = min(max(self.rot[0], -1.57), 1.57) while self.rot[1] > 6.282: self.rot[1] -= 6.282 while self.rot[1] < 0: self.rot[1] += 6.282 return True elif evt.middleIsDown and self.allowTrans: trans = (Quaternion(self.orientation) * Vector(evt.rel[0], evt.rel[1], 0) * self.moveScale) self.trans += trans return True self.lastMouseEvt = evt return False class PointNShootCameraController(FreelookCameraController): """ A camera that is rather dumb and just points at a central location and follows it around """ name = "PointNShootCam" def __init__(self): FreelookCameraController.__init__(self) class PointPhysicalCameraController(FreelookCameraController): """ A camera that has a physical body which causes it to avoid obstacles and also float around more organically """ name = "PointPhysCam" def __init__(self): FreelookCameraController.__init__(self) class DefaultCameraController(OrbitCameraController): """ Default camera controller if none is selected """ name = "DefaultCam" def __init__(self): OrbitCameraController.__init__(self)