NovaBryan/EVE_Dump
Log | Files | 📦 下载最新源码 (.zip) | Refs | README

baseCamera.py (35038B)


      1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\eve\client\script\ui\camera\baseCamera.py
      2 import logging
      3 import math
      4 import blue
      5 from carbon.common.lib.const import SEC
      6 from carbon.common.script.util.mathCommon import FloatCloseEnough
      7 from carbonui.uicore import uicore
      8 import evecamera
      9 import evegraphics.settings as gfxsettings
     10 from eve.client.script.ui.camera.cameraUtil import GetBall, GetBallPosition, GetCameraInertiaMultiplier, GetDurationByDistance, IsInfVector3, IsNanVector3
     11 import geo2
     12 import trinity
     13 import uthread
     14 from signals import Signal
     15 logger = logging.getLogger(__name__)
     16 
     17 class Camera(object):
     18     __typename__ = None
     19     __notifyevents__ = ['OnSetDevice']
     20     name = 'Camera'
     21     default_fov = 1.0
     22     default_nearClip = 6.0
     23     default_farClip = 10000000.0
     24     default_eyePosition = (0, 500, 1000)
     25     default_atPosition = (0, 0, 0)
     26     default_upDirection = (0, 1, 0)
     27     maxFov = 1.5
     28     minFov = 0.2
     29     kZoomSpeed = 10.0
     30     kZoomStopDist = 1e-05
     31     maxZoom = 100
     32     minZoom = 10000
     33     kZoomPower = 9
     34     kFovSpeed = 10.0
     35     kFovStopDist = 0.001
     36     kOrbitSpeed = 5.0
     37     kOrbitStopAngle = 0.0001
     38     kRotateSpeed = 5.0
     39     kRotateStopDist = 0.0001
     40     kMinRotateX = math.pi
     41     kMinPitch = 0.05
     42     kMaxPitch = math.pi - kMinPitch
     43     kPanSpeed = 8.0
     44     kPanStopDist = 0.001
     45     kMaxAspectRatio = 1.6
     46 
     47     def __init__(self):
     48         sm.RegisterNotify(self)
     49         self._fov = self.default_fov
     50         self._nearClip = self.default_nearClip
     51         self._farClip = self.default_farClip
     52         self._eyePosition = self.default_eyePosition
     53         self._atPosition = self.default_atPosition
     54         self._upDirection = self.default_upDirection
     55         self.panTarget = None
     56         self.panUpdateThread = None
     57         self.zoomTarget = None
     58         self.zoomUpdateThread = None
     59         self.orbitTarget = None
     60         self.orbitUpdateThread = None
     61         self.rotateTarget = None
     62         self._rotateOffset = (0.0, 0.0)
     63         self.rotateUpdateThread = None
     64         self.fovTarget = None
     65         self.fovUpdateThread = None
     66         self._fovOffset = 0.0
     67         self._eyeOffset = None
     68         self._atOffset = None
     69         self._eyeAndAtOffset = None
     70         self._atTransitOffset = None
     71         self._eyeTransitOffset = None
     72         self._effectOffset = None
     73         self.centerOffset = 0
     74         self.verticalOffset = 0
     75         self.isActive = False
     76         self._transitDoneTime = None
     77         self.viewMatrix = trinity.TriView()
     78         self.projectionMatrix = trinity.TriProjection()
     79         self.on_update = Signal('on_update')
     80         self.on_orbit = Signal('on_orbit')
     81         self.on_orbit_end = Signal('on_orbit_end')
     82         self.on_eye_position_changed = Signal('on_eye_position_changed')
     83         self.on_transit_complete = Signal('on_transit_complete')
     84 
     85     def UpdateThread(self):
     86         while self.isActive:
     87             self._Update()
     88             blue.synchro.Yield()
     89 
     90     def _Update(self):
     91         self._eyeOffset = None
     92         self._atOffset = None
     93         self._eyeAndAtOffset = None
     94         self.Update()
     95         self.UpdateProjection()
     96         self.UpdateView()
     97 
     98     def Update(self):
     99         self._UpdateTransitOffset()
    100         self._UpdateEffectOffset()
    101         self._UpdateRotateOffset()
    102         self.on_update()
    103 
    104     def _UpdateRotateOffset(self):
    105         if self._rotateOffset != (0.0, 0.0):
    106             offset = self._GetRotateOffset()
    107             self._AddToAtOffset(offset)
    108 
    109     def _GetRotateOffset(self):
    110         yaw, pitch = self._rotateOffset
    111         rotMat = geo2.MatrixRotationAxis(self.upDirection, -yaw)
    112         eyeAtVec = geo2.Vec3Subtract(self._atPosition, self._eyePosition)
    113         vec = geo2.Vec3Transform(eyeAtVec, rotMat)
    114         axis = geo2.Vec3Normalize(geo2.Vec3Cross(vec, self.upDirection))
    115         rotMat = geo2.MatrixRotationAxis(axis, -pitch)
    116         vec = geo2.Vec3Transform(vec, rotMat)
    117         offset = geo2.Vec3Subtract(vec, eyeAtVec)
    118         return offset
    119 
    120     def _UpdateTransitOffset(self):
    121         if self._atTransitOffset:
    122             self._AddToAtOffset(self._atTransitOffset)
    123         if self._eyeTransitOffset:
    124             self._AddToEyeOffset(self._eyeTransitOffset)
    125 
    126     def ResetCurveStartTime(self):
    127         pass
    128 
    129     def OnSetDevice(self, *args):
    130         if self.isActive:
    131             self.UpdateProjection()
    132 
    133     def OnActivated(self, **kwargs):
    134         self.isActive = True
    135         self.OnTransitEnd()
    136         self.Update()
    137 
    138     def OnDeactivated(self):
    139         self.isActive = False
    140         self.StopAnimations()
    141         self.StopUpdateThreads()
    142         self.OnTransitEnd()
    143         self._eyeAndAtOffset = None
    144         self._atOffset = None
    145         self._eyeOffset = None
    146 
    147     def UpdateProjection(self):
    148         aspectRatio = self.GetAspectRatio()
    149         fov = self._fov - self._fovOffset
    150         if fov <= 0.0:
    151             fov = 1.0
    152             self.ResetCameraPosition()
    153             logger.error('Camera: FOV Set to <= 0.0')
    154         offset = self.GetVerticalOffset() or self.GetCenterOffset()
    155         if offset:
    156             self._UpdateProjectionOffset(aspectRatio, fov)
    157         else:
    158             self._UpdateProjectionFov(aspectRatio, fov)
    159 
    160     def GetCenterOffset(self):
    161         return self.centerOffset
    162 
    163     def GetVerticalOffset(self):
    164         return self.verticalOffset
    165 
    166     def GetAspectRatio(self):
    167         return uicore.uilib.desktop.width / float(uicore.uilib.desktop.height)
    168 
    169     def _UpdateProjectionFov(self, aspectRatio, fov):
    170         fov *= self.GetProjectionFovConst(aspectRatio)
    171         self.projectionMatrix.PerspectiveFov(fov, aspectRatio, self._nearClip, self._farClip)
    172 
    173     def GetProjectionFovConst(self, aspectRatio):
    174         if aspectRatio > self.kMaxAspectRatio:
    175             return self.kMaxAspectRatio / aspectRatio
    176         else:
    177             return 1.0
    178 
    179     def _UpdateProjectionOffset(self, aspectRatio, fov):
    180         tanFov = math.tan(fov / 2.0)
    181         k = self.GetProjectionFovConst(aspectRatio)
    182         dX = k * aspectRatio * self._nearClip * tanFov
    183         dY = k * self._nearClip * tanFov
    184         verticalOffset = self.GetVerticalOffset()
    185         horizontalOffset = self.GetCenterOffset()
    186         left = -dX + dX * horizontalOffset
    187         right = dX + dX * horizontalOffset
    188         top = dY + dY * verticalOffset
    189         bottom = -dY + dY * verticalOffset
    190         self.projectionMatrix.PerspectiveOffCenter(left, right, bottom, top, self._nearClip, self._farClip)
    191 
    192     def UpdateView(self):
    193         self.viewMatrix.SetLookAtPosition(self.eyePosition, self.atPosition, self._upDirection)
    194 
    195     def OffsetAtPosition(self, atPosition):
    196         if self._atOffset:
    197             atPosition = geo2.Vec3AddD(atPosition, self._atOffset)
    198         if self._eyeAndAtOffset:
    199             atPosition = geo2.Vec3AddD(atPosition, self._eyeAndAtOffset)
    200         return atPosition
    201 
    202     def OffsetEyePosition(self, eyePosition):
    203         if self._eyeOffset:
    204             eyePosition = geo2.Vec3AddD(eyePosition, self._eyeOffset)
    205         if self._eyeAndAtOffset:
    206             eyePosition = geo2.Vec3AddD(eyePosition, self._eyeAndAtOffset)
    207         return eyePosition
    208 
    209     def _AddToEyeOffset(self, offset):
    210         if IsInfVector3(offset) or IsNanVector3(offset):
    211             raise DestinyBallInvalid('Camera: Attempting to assign an invalid eyeOffset value: %s, %s' % (repr(offset), self))
    212         if not self._eyeOffset:
    213             self._eyeOffset = offset
    214         else:
    215             self._eyeOffset = geo2.Vec3AddD(self._eyeOffset, offset)
    216 
    217     def _AddToAtOffset(self, offset):
    218         if IsInfVector3(offset) or IsNanVector3(offset):
    219             raise DestinyBallInvalid('Camera: Attempting to assign an invalid atOffset value: %s, %s' % (repr(offset), self))
    220         if not self._atOffset:
    221             self._atOffset = offset
    222         else:
    223             self._atOffset = geo2.Vec3AddD(self._atOffset, offset)
    224 
    225     def _SetAtOffset(self, offset):
    226         if offset is not None and (IsInfVector3(offset) or IsNanVector3(offset)):
    227             raise DestinyBallInvalid('Camera: Attempting to assign an invalid atOffset value: %s, %s' % (repr(offset), self))
    228         self._atOffset = offset
    229 
    230     def SetEffectOffset(self, offset):
    231         self._effectOffset = offset
    232 
    233     def _UpdateEffectOffset(self):
    234         if self._effectOffset:
    235             self._AddToEyeAndAtOffset(self._effectOffset)
    236 
    237     def _AddToEyeAndAtOffset(self, offset):
    238         if IsInfVector3(offset) or IsNanVector3(offset):
    239             raise DestinyBallInvalid('Camera: Attempting to assign an invalid eyeAndAtOffset value: %s, %s' % (repr(offset), self))
    240         if not self._eyeAndAtOffset:
    241             self._eyeAndAtOffset = offset
    242         else:
    243             self._eyeAndAtOffset = geo2.Vec3Add(self._eyeAndAtOffset, offset)
    244 
    245     def GetXAxis(self):
    246         t = self.viewMatrix.transform
    247         return (t[0][0], t[1][0], t[2][0])
    248 
    249     def GetYAxis(self):
    250         t = self.viewMatrix.transform
    251         return (t[0][1], t[1][1], t[2][1])
    252 
    253     def GetZAxis(self):
    254         t = self.viewMatrix.transform
    255         return (t[0][2], t[1][2], t[2][2])
    256 
    257     def SetViewVector(self, viewVector):
    258         self.StopUpdateThreads()
    259         atPosition = self.GetAtPosition()
    260         eyePosition = self.GetEyePosition()
    261         eyePositionLocal = geo2.Subtract(atPosition, eyePosition)
    262         eyeDistance = geo2.Vec3Length(eyePositionLocal)
    263         newEyePosition = geo2.Vec3Scale(viewVector, eyeDistance)
    264         self.eyePosition = geo2.Vec3Add(newEyePosition, atPosition)
    265         if not self.isActive:
    266             self.Update()
    267 
    268     def GetViewVector(self):
    269         return self.GetLookAtDirection()
    270 
    271     def GetYaw(self):
    272         x, _, z = self.GetLookAtDirection()
    273         return -(math.atan2(z, x) + math.pi / 2)
    274 
    275     def SetYaw(self, yaw):
    276         rotMat = geo2.MatrixRotationY(yaw - math.pi)
    277         eyePos = geo2.Vec3Subtract(self._eyePosition, self._atPosition)
    278         x = math.sqrt(eyePos[0] ** 2 + eyePos[2] ** 2)
    279         vec = (0, eyePos[1], x)
    280         self._eyePosition = geo2.Vec3Add(geo2.Vec3Transform(vec, rotMat), self._atPosition)
    281 
    282     yaw = property(GetYaw, SetYaw)
    283 
    284     def GetPitch(self):
    285         x, y, z = self.GetLookAtDirection()
    286         pitch = math.atan2(math.sqrt(z ** 2 + x ** 2), -y)
    287         return math.pi - pitch
    288 
    289     def SetPitch(self, pitch):
    290         pitch = self.ClampPitch(pitch)
    291         axis = geo2.Vec3Cross(self.upDirection, self.GetLookAtDirection())
    292         rotMat = geo2.MatrixRotationAxis(axis, pitch)
    293         vec = (0, self.GetZoomDistance(), 0)
    294         self._eyePosition = geo2.Vec3Add(self._atPosition, geo2.Vec3Transform(vec, rotMat))
    295 
    296     pitch = property(GetPitch, SetPitch)
    297 
    298     def GetRotationQuat(self):
    299         yaw = self.GetYaw()
    300         pitch = 3 * math.pi / 2 - self.GetPitch()
    301         return geo2.QuaternionRotationSetYawPitchRoll(yaw, pitch, 0.0)
    302 
    303     def GetLookAtDirection(self):
    304         return geo2.Vec3Direction(self._eyePosition, self.GetZoomToPoint())
    305 
    306     def GetLookAtDirectionWithOffset(self):
    307         eyePos = geo2.Vec3AddD(self._eyePosition, self._eyeOffset) if self._eyeOffset else self._eyePosition
    308         atPos = geo2.Vec3AddD(self._atPosition, self._atOffset) if self._atOffset else self._atPosition
    309         return geo2.Vec3Direction(eyePos, atPos)
    310 
    311     def Pan(self, dx = 0, dy = 0, dz = 0):
    312         if self.panTarget is None:
    313             self.panTarget = geo2.Vector(0, 0, 0)
    314         if dx:
    315             self._AddToPanTarget(dx, self.GetXAxis())
    316         if dy:
    317             self._AddToPanTarget(dy, self.GetYAxis())
    318         if dz:
    319             self._AddToPanTarget(dz, self.GetZAxis())
    320         if not self.panUpdateThread:
    321             self.panUpdateThread = uthread.new(self.PanUpdateThread)
    322 
    323     def _AddToPanTarget(self, dx, axis):
    324         self.panTarget = geo2.Vec3Add(self.panTarget, geo2.Scale(axis, dx))
    325         self._ClampPanTarget()
    326 
    327     def _ClampPanTarget(self):
    328         pass
    329 
    330     def PanTo(self, diff):
    331         self.panTarget = geo2.Vector(*diff)
    332         if not self.panUpdateThread:
    333             self.panUpdateThread = uthread.new(self.PanUpdateThread)
    334 
    335     def PanAxis(self, axis, amount):
    336         if self.panTarget is None:
    337             self.panTarget = geo2.Vector(0, 0, 0)
    338         self._AddToPanTarget(amount, axis)
    339         if not self.panUpdateThread:
    340             self.panUpdateThread = uthread.new(self.PanUpdateThread)
    341 
    342     def PanUpdateThread(self):
    343         try:
    344             while True:
    345                 if self.panTarget is None:
    346                     break
    347                 if self._IsPanTargetOutOfBounds():
    348                     return
    349                 distLeft = geo2.Vec3LengthD(self.panTarget)
    350                 if distLeft == 0:
    351                     break
    352                 if distLeft < self.kPanStopDist:
    353                     dist = 1.0
    354                 else:
    355                     dist = min(1.0, self._GetPanSpeed() / blue.os.fps)
    356                 toMove = geo2.Vec3ScaleD(self.panTarget, dist)
    357                 self.SetEyePosition(geo2.Vec3Add(self._eyePosition, toMove))
    358                 self.SetAtPosition(geo2.Vec3Add(self._atPosition, toMove))
    359                 self.panTarget = geo2.Vec3SubtractD(self.panTarget, toMove)
    360                 if dist == 1.0:
    361                     break
    362                 blue.synchro.Yield()
    363 
    364         finally:
    365             self.panUpdateThread = None
    366             self.panTarget = None
    367 
    368     def _IsPanTargetOutOfBounds(self):
    369         if geo2.Vec3Length(self.eyePosition) > 0.99 * evecamera.LOOKATRANGE_MAX_NEW:
    370             return geo2.Vec3Dot(self.atPosition, self.panTarget) > 0.0
    371         return False
    372 
    373     def IsZoomedOutCompletely(self):
    374         zoomValue = self.GetZoomValue()
    375         return zoomValue > 1.0 - self.kZoomStopDist
    376 
    377     def IsZoomedInCompletely(self):
    378         zoomValue = self.GetZoomValue()
    379         return zoomValue == 0.0
    380 
    381     def GetZoomValue(self):
    382         if self.zoomTarget is not None:
    383             zoomValue = self.zoomTarget
    384         else:
    385             zoomValue = self.GetZoomProportion()
    386         return zoomValue
    387 
    388     def Zoom(self, dz):
    389         if dz < 0 and self.IsZoomedInCompletely():
    390             return
    391         if dz > 0 and self.IsZoomedOutCompletely():
    392             return
    393         if self.zoomTarget is None:
    394             self.zoomTarget = self.GetZoomProportion()
    395         self.zoomTarget += 0.3 * dz
    396         self.zoomTarget = self._ClampZoomProp(self.zoomTarget)
    397         if not self.zoomUpdateThread:
    398             self.zoomUpdateThread = uthread.new(self.ZoomUpdateThread)
    399 
    400     def SetZoomTarget(self, proportion):
    401         self.zoomTarget = self._ClampZoomProp(proportion)
    402         if not self.zoomUpdateThread:
    403             self.zoomUpdateThread = uthread.new(self.ZoomUpdateThread)
    404 
    405     def ZoomUpdateThread(self):
    406         try:
    407             zoomProp = self.GetZoomProportion()
    408             while True:
    409                 if self.zoomTarget is None:
    410                     break
    411                 distLeft = self.zoomTarget - zoomProp
    412                 if not distLeft:
    413                     break
    414                 distProp = self.GetZoomDistance() / self.minZoom
    415                 zoomSpeed = self._GetZoomSpeed()
    416                 moveProp = zoomSpeed / blue.os.fps
    417                 if math.fabs(distLeft) < self.kZoomStopDist:
    418                     moveProp *= self.kZoomStopDist / math.fabs(distLeft)
    419                 moveProp = min(moveProp, 1.0)
    420                 zoomProp += distLeft * moveProp
    421                 self.SetZoom(zoomProp)
    422                 if moveProp == 1.0:
    423                     break
    424                 blue.synchro.Yield()
    425 
    426         finally:
    427             self.zoomUpdateThread = None
    428             self.zoomTarget = None
    429 
    430     def _ClampZoomProp(self, ret):
    431         minZoomProp = self.GetMinZoomProp()
    432         return max(minZoomProp, min(ret, 1.0))
    433 
    434     def _ClampZoomPropLinear(self, ret):
    435         return max(0.0, min(ret, 1.0))
    436 
    437     def GetMinZoomProp(self):
    438         if FloatCloseEnough(self.minZoom, 0.0):
    439             return 1.0
    440         return min(0.2 + self.maxZoom / self.minZoom, 1.0)
    441 
    442     def GetZoomDistanceByZoomProportion(self, zoomProp):
    443         return self.maxZoom + zoomProp ** self.kZoomPower * (self.minZoom - self.maxZoom)
    444 
    445     def GetZoom(self):
    446         return self.GetZoomProportion()
    447 
    448     def SetZoom(self, proportion):
    449         proportion = self._ClampZoomProp(proportion)
    450         distance = self.GetZoomDistanceByZoomProportion(proportion)
    451         self._SetZoomDistance(distance)
    452 
    453     zoom = property(GetZoom, SetZoom)
    454 
    455     def SetZoomLinear(self, zoomProp):
    456         zoomProp = max(0.0, min(zoomProp, 1.0))
    457         distance = self.maxZoom + zoomProp * (self.minZoom - self.maxZoom)
    458         self._SetZoomDistance(distance)
    459 
    460     def _SetZoomDistance(self, distance):
    461         direction = self.GetLookAtDirection()
    462         if IsNanVector3(direction):
    463             return
    464         zoomVec = geo2.Vec3Scale(direction, distance)
    465         self.TrySetEyePosition(geo2.Vec3Add(self.GetZoomToPoint(), zoomVec))
    466 
    467     def GetZoomLinear(self):
    468         return self.GetZoomProportionLinear()
    469 
    470     zoomLinear = property(GetZoomLinear, SetZoomLinear)
    471 
    472     def GetZoomToPoint(self):
    473         if self._atTransitOffset:
    474             return geo2.Vec3Add(self._atPosition, self._atTransitOffset)
    475         return self._atPosition
    476 
    477     def FovZoom(self, dz):
    478         if self.fovTarget is not None:
    479             self.SetFovTarget(self.fovTarget + dz)
    480         else:
    481             self.SetFovTarget(self.fov + dz)
    482 
    483     def SetFovTarget(self, value):
    484         if self.fov == value:
    485             return
    486         if math.fabs(self.fov - value) < 0.001 and not self.fovUpdateThread:
    487             self.SetFov(value)
    488         else:
    489             self.fovTarget = self._EnforceMinMaxFov(value)
    490             if not self.fovUpdateThread:
    491                 self.fovUpdateThread = uthread.new(self.FovUpdateThread)
    492 
    493     def FovUpdateThread(self):
    494         try:
    495             while True:
    496                 if self.fovTarget is None:
    497                     break
    498                 distLeft = self.fovTarget - self.fov
    499                 if not distLeft:
    500                     break
    501                 moveProp = self._GetFovSpeed() / blue.os.fps
    502                 if math.fabs(distLeft) < self.kFovStopDist:
    503                     moveProp *= self.kFovStopDist / math.fabs(distLeft)
    504                 moveProp = min(moveProp, 1.0)
    505                 fov = self.fov + moveProp * distLeft
    506                 self.fov = self._EnforceMinMaxFov(fov)
    507                 if fov != self.fov:
    508                     break
    509                 if moveProp == 1.0:
    510                     break
    511                 blue.synchro.Yield()
    512 
    513         finally:
    514             self.fovUpdateThread = None
    515             self.fovTarget = None
    516 
    517     def _GetFovSpeed(self):
    518         multiplier = GetCameraInertiaMultiplier()
    519         return self.kFovSpeed * multiplier
    520 
    521     def _EnforceMinMaxFov(self, value):
    522         if value >= self.maxFov:
    523             return self.maxFov
    524         if value <= self.minFov:
    525             return self.minFov
    526         return value
    527 
    528     def StopUpdateThreads(self, skipFov = False):
    529         self.zoomTarget = None
    530         self.panTarget = None
    531         self.orbitTarget = None
    532         self.rotateTarget = None
    533         if not skipFov:
    534             self.fovTarget = None
    535 
    536     def GetZoomDistance(self):
    537         dist = geo2.Vec3Distance(self._eyePosition, self.GetZoomToPoint())
    538         if math.isinf(dist):
    539             egoBall = None
    540             ballpark = sm.GetService('michelle').GetBallpark()
    541             if ballpark:
    542                 egoBall = ballpark.ego
    543             wannaBeEgo = sm.GetService('gameui').wannaBeEgo
    544             logger.warning('Error: Infinite camera distance to %s/%s: %s, %s' % (str(egoBall),
    545              wannaBeEgo,
    546              repr(dist),
    547              self))
    548             return 1.0
    549         return dist
    550 
    551     def SetZoomDistance(self, distance):
    552         zoomVec = geo2.Vec3Scale(self.GetLookAtDirection(), distance)
    553         self.SetEyePosition(geo2.Vec3Add(self._atPosition, zoomVec))
    554 
    555     zoomDistance = property(GetZoomDistance, SetZoomDistance)
    556 
    557     def GetZoomProportion(self):
    558         zoomDist = max(self.maxZoom, self.GetZoomDistance())
    559         zoomProp = self.GetZoomProportionByZoomDistance(zoomDist)
    560         return self._ClampZoomProp(zoomProp)
    561 
    562     def GetZoomProportionLinear(self):
    563         zoomDist = max(self.maxZoom, self.GetZoomDistance())
    564         zoomProp = self.GetZoomProportionByZoomDistanceLinear(zoomDist)
    565         return self._ClampZoomPropLinear(zoomProp)
    566 
    567     def GetZoomProportionByZoomDistance(self, zoomDist):
    568         zoomProp = self.GetZoomProportionByZoomDistanceLinear(zoomDist)
    569         zoomProp = max(0.0, zoomProp)
    570         return zoomProp ** (1.0 / self.kZoomPower)
    571 
    572     def GetZoomProportionByZoomDistanceLinear(self, zoomDist):
    573         if FloatCloseEnough(self.minZoom, self.maxZoom):
    574             return 0.0
    575         zoomProp = (zoomDist - self.maxZoom) / (self.minZoom - self.maxZoom)
    576         return zoomProp
    577 
    578     def SetMaxZoom(self, value):
    579         if value >= self.minZoom:
    580             logger.error('Camera: Cannot set maxZoom to a value larger than minZoom: value=%s, minZoom:%s', value, self.minZoom)
    581         else:
    582             self.maxZoom = value
    583 
    584     def SetMinZoom(self, value):
    585         if value <= self.maxZoom:
    586             logger.error('Camera: Cannot set minZoom to a value smaller than maxZoom: value=%s, maxZoom:%s', value, self.maxZoom)
    587         else:
    588             self.minZoom = value
    589 
    590     def SetMinMaxZoom(self, minZoom, maxZoom):
    591         if minZoom <= maxZoom:
    592             logger.error('Camera: Cannot set minZoom to a value smaller than maxZoom: minZoom=%s, maxZoom=%s', minZoom, maxZoom)
    593         self.minZoom = minZoom
    594         self.maxZoom = maxZoom
    595 
    596     def Orbit(self, dx = 0, dy = 0):
    597         if not self.orbitTarget:
    598             self.orbitTarget = (0, self.GetAngleLookAtToUpDirection())
    599         if gfxsettings.Get(gfxsettings.UI_CAMERA_INVERT_Y):
    600             dy *= -1
    601         yaw = self.orbitTarget[0] - dx
    602         pitch = self.orbitTarget[1] - dy / 2.0
    603         pitch = self.ClampPitch(pitch)
    604         self.orbitTarget = [yaw, pitch]
    605         if not self.orbitUpdateThread:
    606             self.orbitUpdateThread = uthread.new(self.OrbitUpdateThread)
    607         self.on_orbit()
    608 
    609     def ClampPitch(self, pitch):
    610         return max(self.kMinPitch, min(pitch, self.kMaxPitch))
    611 
    612     def OrbitUpdateThread(self):
    613         try:
    614             while True:
    615                 if self.orbitTarget is None:
    616                     break
    617                 vLookAt = self.GetLookAtDirectionWithOffset()
    618                 currPitch = self.GetAngleLookAtToUpDirection()
    619                 offset = self.GetOrbitPoint()
    620                 self.eyePosition = geo2.Subtract(self._eyePosition, offset)
    621                 yawRemaining = self._UpdateYaw()
    622                 pitchRemaining = self._UpdatePitch(currPitch, vLookAt)
    623                 self.SetEyePosition(geo2.Add(self._eyePosition, offset))
    624                 if not pitchRemaining and not yawRemaining:
    625                     break
    626                 blue.synchro.Yield()
    627 
    628         finally:
    629             self.orbitUpdateThread = None
    630             self.orbitTarget = None
    631             self.on_orbit_end()
    632 
    633     def GetOrbitPoint(self):
    634         if self._atOffset:
    635             offset = geo2.Vec3Add(self._atPosition, self._atOffset)
    636         else:
    637             offset = self._atPosition
    638         return offset
    639 
    640     def _UpdatePitch(self, currPitch, vLookAt):
    641         targetPitch = self.orbitTarget[1]
    642         pitchRemaining = currPitch - targetPitch
    643         if pitchRemaining:
    644             if math.fabs(pitchRemaining) < self.kOrbitStopAngle:
    645                 pitch = pitchRemaining
    646                 pitchRemaining = None
    647             else:
    648                 pitch = pitchRemaining * self._GetOrbitSpeed()
    649             axis = geo2.Vec3Cross(vLookAt, self.upDirection)
    650             rotPitch = geo2.MatrixRotationAxis(axis, pitch)
    651             self.SetEyePosition(geo2.Vec3Transform(self._eyePosition, rotPitch))
    652         return pitchRemaining
    653 
    654     def Rotate(self, dx = 0, dy = 0):
    655         if self.IsInTransit():
    656             return
    657         if not self.rotateTarget:
    658             self.rotateTarget = (0.0, 0.0)
    659         self.StopOrbitUpdate()
    660         x = dx + self.rotateTarget[0]
    661         x = self._ClampRotateX(x)
    662         if gfxsettings.Get(gfxsettings.UI_CAMERA_INVERT_Y):
    663             dy *= -1
    664         y = dy + self.rotateTarget[1]
    665         y = self._ClampRotateY(y)
    666         self._SetRotateTarget(x, y)
    667 
    668     def _ClampRotateX(self, x):
    669         x = max(-self.kMinRotateX, min(x, self.kMinRotateX))
    670         return x
    671 
    672     def _ClampRotateY(self, y):
    673         pitch = math.pi - self.GetPitch()
    674         yMax = min(math.pi - self.kMinPitch - pitch, 0.3 * math.pi)
    675         yMin = max(-(pitch - self.kMinPitch), -0.3 * math.pi)
    676         y = max(yMin, min(y, yMax))
    677         return y
    678 
    679     def StopOrbitUpdate(self):
    680         self.orbitTarget = None
    681 
    682     def _SetRotateTarget(self, x, y):
    683         self.rotateTarget = (x, y)
    684         if not self.rotateUpdateThread:
    685             self.rotateUpdateThread = uthread.new(self.RotateUpdateThread)
    686 
    687     def ResetRotate(self):
    688         self._SetRotateTarget(0.0, 0.0)
    689 
    690     def IsRotated(self):
    691         return self.rotateTarget not in (None, (0.0, 0.0))
    692 
    693     def RotateUpdateThread(self):
    694         try:
    695             while True:
    696                 if self.rotateTarget is None:
    697                     break
    698                 distLeft = geo2.Vec2Length(geo2.Vec2Subtract(self.rotateTarget, self._rotateOffset))
    699                 if not distLeft:
    700                     break
    701                 moveProp = self._GetRotateSpeed() / blue.os.fps
    702                 if math.fabs(distLeft) < self.kRotateStopDist:
    703                     moveProp *= self.kRotateStopDist / math.fabs(distLeft)
    704                 moveProp = min(moveProp, 1.0)
    705                 self._rotateOffset = geo2.Lerp(self._rotateOffset, self.rotateTarget, moveProp)
    706                 blue.synchro.Yield()
    707 
    708         finally:
    709             self.rotateUpdateThread = None
    710 
    711     def _GetRotateSpeed(self):
    712         multiplier = GetCameraInertiaMultiplier()
    713         return self.kRotateSpeed * multiplier
    714 
    715     def _GetOrbitSpeed(self):
    716         multiplier = GetCameraInertiaMultiplier()
    717         speed = self.kOrbitSpeed * multiplier / blue.os.fps
    718         return min(1.0, speed)
    719 
    720     def _GetZoomSpeed(self):
    721         multiplier = GetCameraInertiaMultiplier()
    722         return self.kZoomSpeed * multiplier
    723 
    724     def _GetPanSpeed(self):
    725         multiplier = GetCameraInertiaMultiplier()
    726         return self.kPanSpeed * multiplier
    727 
    728     def _UpdateYaw(self):
    729         yawRemaining = self.orbitTarget[0]
    730         if yawRemaining:
    731             if math.fabs(yawRemaining) < self.kOrbitStopAngle:
    732                 yaw = yawRemaining
    733                 yawRemaining = None
    734             else:
    735                 yaw = yawRemaining * self._GetOrbitSpeed()
    736             rotYaw = geo2.MatrixRotationAxis(self.upDirection, yaw)
    737             self.SetEyePosition(geo2.Vec3Transform(self._eyePosition, rotYaw))
    738             self.orbitTarget[0] -= yaw
    739         return yawRemaining
    740 
    741     def GetAngleLookAtToUpDirection(self):
    742         try:
    743             vLookAt = self.GetLookAtDirectionWithOffset()
    744             return math.acos(geo2.Vec3Dot(vLookAt, self.upDirection) / (geo2.Vec3Length(vLookAt) * geo2.Vec3Length(self.upDirection)))
    745         except (ValueError, ZeroDivisionError):
    746             return 0.0
    747 
    748     def GetActiveThreads(self):
    749         return (self.panTarget, self.orbitTarget, self.zoomTarget)
    750 
    751     def GetDistanceFromLookAt(self):
    752         return geo2.Vec3Distance(self.eyePosition, self.atPosition)
    753 
    754     def LookAt(self, itemID, radius = None, **kwargs):
    755         ball = GetBall(itemID)
    756         if not ball:
    757             return
    758         if not radius:
    759             radius = ball.radius * 2
    760         atPos1 = GetBallPosition(ball)
    761         direction = self.GetLookAtDirectionWithOffset()
    762         eyePos1 = geo2.Vec3Add(atPos1, geo2.Vec3Scale(direction, radius))
    763         duration = GetDurationByDistance(self.eyePosition, eyePos1, 0.3, 0.6)
    764         self.TransitTo(atPos1, eyePos1, duration=duration)
    765 
    766     def LookAtMaintainDistance(self, itemID):
    767         self.LookAt(itemID)
    768 
    769     def TransitTo(self, atPosition = None, eyePosition = None, duration = 1.0, smoothing = 0.1, numPoints = 1000, timeOffset = 0.0, callback = None, sleep = False):
    770         self.Transit(self.atPosition, self.eyePosition, atPosition, eyePosition, duration, smoothing, numPoints, timeOffset, callback, sleep)
    771 
    772     def Transit(self, atPos0, eyePos0, atPos1, eyePos1, duration = 1.0, smoothing = 0.1, numPoints = 1000, timeOffset = 0.0, callback = None, sleep = False):
    773         self.StopEyeAndAtAnimation()
    774         if self._atTransitOffset:
    775             atPos0 = geo2.Vec3Add(atPos0, self._atTransitOffset)
    776         if self._eyeTransitOffset:
    777             eyePos0 = geo2.Vec3Add(eyePos0, self._eyeTransitOffset)
    778         self.SetAtPosition(atPos1)
    779         self.SetEyePosition(eyePos1)
    780         self._atTransitOffset = geo2.Vec3Subtract(atPos0, atPos1)
    781         self._eyeTransitOffset = geo2.Vec3Subtract(eyePos0, eyePos1)
    782         uicore.animations.MorphVector3(self, '_atTransitOffset', self._atTransitOffset, (0, 0, 0), duration=duration, timeOffset=timeOffset, callback=callback)
    783         uicore.animations.MorphVector3(self, '_eyeTransitOffset', self._eyeTransitOffset, (0, 0, 0), duration=duration, timeOffset=timeOffset, callback=self.OnTransitEnd, sleep=sleep)
    784         self.SetTransitDuration(duration)
    785 
    786     def StopEyeAndAtAnimation(self):
    787         uicore.animations.StopAnimation(self, 'atPosition')
    788         uicore.animations.StopAnimation(self, 'eyePosition')
    789         self.OnTransitEnd()
    790         self.panTarget = None
    791         self.orbitTarget = None
    792         self.rotateTarget = None
    793         self.zoomTarget = None
    794         self.fovTarget = None
    795 
    796     def SetTransitDuration(self, duration):
    797         self._transitDoneTime = blue.os.GetWallclockTime() + SEC * duration
    798 
    799     def IsInTransit(self):
    800         return self._transitDoneTime is not None and blue.os.GetWallclockTime() < self._transitDoneTime
    801 
    802     def OnTransitEnd(self):
    803         uicore.animations.StopAnimation(self, '_atTransitOffset')
    804         uicore.animations.StopAnimation(self, '_eyeTransitOffset')
    805         self._atTransitOffset = None
    806         self._eyeTransitOffset = None
    807         self.on_transit_complete()
    808 
    809     def StopAnimations(self):
    810         uicore.animations.StopAllAnimations(self)
    811 
    812     def GetFov(self):
    813         return self._fov
    814 
    815     def SetFov(self, value):
    816         if math.isnan(value):
    817             self.ResetCameraPosition()
    818             raise DestinyBallInvalid('Attempting to assign NaN value')
    819         self._fov = value
    820 
    821     fov = property(GetFov, SetFov)
    822 
    823     def GetFovOffset(self):
    824         return self._fovOffset
    825 
    826     def SetFovOffset(self, value):
    827         if math.isnan(value):
    828             self.ResetCameraPosition()
    829             raise DestinyBallInvalid('Attempting to assign NaN value')
    830         self._fovOffset = value
    831 
    832     fovOffset = property(GetFovOffset, SetFovOffset)
    833 
    834     def GetNearClip(self):
    835         return self._nearClip
    836 
    837     def SetNearClip(self, value):
    838         if math.isnan(value):
    839             self.ResetCameraPosition()
    840             raise DestinyBallInvalid('Attempting to assign NaN value')
    841         self._nearClip = value
    842 
    843     nearClip = property(GetNearClip, SetNearClip)
    844 
    845     def GetFarClip(self):
    846         return self._farClip
    847 
    848     def SetFarClip(self, value):
    849         if math.isnan(value):
    850             self.ResetCameraPosition()
    851             raise ValueError('Attempting to assign NaN value')
    852         self._farClip = value
    853 
    854     farClip = property(GetFarClip, SetFarClip)
    855 
    856     def GetEyePosition(self):
    857         return self.OffsetEyePosition(self._eyePosition)
    858 
    859     def SetEyePosition(self, value):
    860         if IsNanVector3(value) or IsInfVector3(value):
    861             raise DestinyBallInvalid('Attempting to assign an invalid eyePosition value: %s, %s' % (repr(value), self))
    862         elif value == self._atPosition:
    863             raise ValueError('Camera: Setting eyePosition to same value as atPosition: %s, %s' % (repr(value), self))
    864         else:
    865             self._eyePosition = value
    866             self.on_eye_position_changed(value)
    867 
    868     eyePosition = property(GetEyePosition, SetEyePosition)
    869 
    870     def IsValidEyePosition(self, value, atPosition = None):
    871         if atPosition is None:
    872             atPosition = self._atPosition
    873         return not (IsNanVector3(value) or IsInfVector3(value)) and value != atPosition
    874 
    875     def TrySetEyePosition(self, value):
    876         if not self.IsValidEyePosition(value):
    877             logger.debug('Camera: Ignoring invalid eyePosition %r, %s', value, self)
    878             return False
    879         self.SetEyePosition(value)
    880         return True
    881 
    882     def GetAtPosition(self):
    883         return self.OffsetAtPosition(self._atPosition)
    884 
    885     def SetAtPosition(self, value):
    886         if IsNanVector3(value) or IsInfVector3(value):
    887             raise DestinyBallInvalid('Attempting to assign an invalid atPosition value: %s, %s' % (repr(value), self))
    888         elif value == self._eyePosition:
    889             raise ValueError('Camera: Setting atPosition to same value as eyePosition: %s, %s' % (repr(value), self))
    890         else:
    891             self._atPosition = value
    892 
    893     atPosition = property(GetAtPosition, SetAtPosition)
    894 
    895     def GetUpDirection(self):
    896         return self._upDirection
    897 
    898     def SetUpDirection(self, value):
    899         self._upDirection = geo2.Vec3Normalize(value)
    900 
    901     upDirection = property(GetUpDirection, SetUpDirection)
    902 
    903     def GetRoll(self):
    904         viewDir = geo2.Vec3Direction(self.atPosition, self._eyePosition)
    905         right = geo2.Vec3Normalize(geo2.Vec3Cross(viewDir, (0, 1, 0)))
    906         cosTheta = geo2.Vec3Dot(self.upDirection, geo2.Vec3Normalize(geo2.Vec3Cross(right, viewDir)))
    907         cosTheta = max(-1.0, min(1.0, cosTheta))
    908         return math.acos(cosTheta)
    909 
    910     def GetTrackItemID(self):
    911         return None
    912 
    913     def ResetCamera(self, *args):
    914         pass
    915 
    916     def ResetCameraPosition(self):
    917         self._atPosition = self.default_atPosition
    918         self._eyePosition = self.default_eyePosition
    919         self._fov = self.default_fov
    920         self.trackTarget = None
    921 
    922     def ProjectWorldToCamera(self, vec):
    923         return geo2.Vec3Transform(vec, self.viewMatrix.transform)
    924 
    925     def IsInFrontOfCamera(self, vec):
    926         vec = self.ProjectWorldToCamera(vec)
    927         return vec[2] < -self.nearClip
    928 
    929     def EnforceMinZoom(self):
    930         if self.GetZoomDistance() < self.maxZoom:
    931             self.SetZoom(self.GetMinZoomProp())
    932 
    933     def EnforceMaxZoom(self):
    934         if self.GetZoomDistance() > self.minZoom:
    935             self.SetZoom(1.0)
    936 
    937     def __repr__(self):
    938         return '<%s at %s. eyePos=%s, atPos=%s, eyeOffset=%s, atOffset=%s, atTransitOffset =%s, eyeAndAtOffset=%s, minZoom=%s, maxZoom=%s, isActive=%s, fov=%s>' % (self.__class__.__name__,
    939          id(self),
    940          repr(self._eyePosition),
    941          repr(self._atPosition),
    942          repr(self._eyeOffset),
    943          repr(self._atOffset),
    944          repr(self._atTransitOffset),
    945          repr(self._eyeAndAtOffset),
    946          repr(self.minZoom),
    947          repr(self.maxZoom),
    948          repr(self.isActive),
    949          repr(self.fov))
    950 
    951 
    952 class DestinyBallInvalid(ValueError):
    953     pass