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