1import weakref 2from numbers import Number as numeric_type 3 4import numpy as np 5 6from yt.funcs import ensure_numpy_array, is_sequence 7from yt.units.yt_array import YTArray, YTQuantity 8from yt.utilities.math_utils import get_rotation_matrix 9from yt.utilities.orientation import Orientation 10 11from .lens import Lens, lenses 12from .utils import data_source_or_all 13 14 15def _sanitize_camera_property_units(value, scene): 16 if is_sequence(value): 17 if len(value) == 1: 18 return _sanitize_camera_property_units(value[0], scene) 19 elif isinstance(value, YTArray) and len(value) == 3: 20 return scene.arr(value).in_units("unitary") 21 elif ( 22 len(value) == 2 23 and isinstance(value[0], numeric_type) 24 and isinstance(value[1], str) 25 ): 26 return scene.arr([scene.arr(value[0], value[1]).in_units("unitary")] * 3) 27 if len(value) == 3: 28 if all([is_sequence(v) for v in value]): 29 if all( 30 [ 31 isinstance(v[0], numeric_type) and isinstance(v[1], str) 32 for v in value 33 ] 34 ): 35 return scene.arr([scene.arr(v[0], v[1]) for v in value]) 36 else: 37 raise RuntimeError( 38 f"Cannot set camera width to invalid value '{value}'" 39 ) 40 return scene.arr(value, "unitary") 41 else: 42 if isinstance(value, (YTQuantity, YTArray)): 43 return scene.arr([value.d] * 3, value.units).in_units("unitary") 44 elif isinstance(value, numeric_type): 45 return scene.arr([value] * 3, "unitary") 46 raise RuntimeError(f"Cannot set camera width to invalid value '{value}'") 47 48 49class Camera(Orientation): 50 r"""A representation of a point of view into a Scene. 51 52 It is defined by a position (the location of the camera 53 in the simulation domain,), a focus (the point at which the 54 camera is pointed), a width (the width of the snapshot that will 55 be taken, a resolution (the number of pixels in the image), and 56 a north_vector (the "up" direction in the resulting image). A 57 camera can use a variety of different Lens objects. 58 59 Parameters 60 ---------- 61 scene: A :class:`yt.visualization.volume_rendering.scene.Scene` object 62 A scene object that the camera will be attached to. 63 data_source: :class:`AMR3DData` or :class:`Dataset`, optional 64 This is the source to be rendered, which can be any arbitrary yt 65 data object or dataset. 66 lens_type: string, optional 67 This specifies the type of lens to use for rendering. Current 68 options are 'plane-parallel', 'perspective', and 'fisheye'. See 69 :class:`yt.visualization.volume_rendering.lens.Lens` for details. 70 Default: 'plane-parallel' 71 auto: boolean 72 If True, build smart defaults using the data source extent. This 73 can be time-consuming to iterate over the entire dataset to find 74 the positional bounds. Default: False 75 76 Examples 77 -------- 78 79 In this example, the camera is set using defaults that are chosen 80 to be reasonable for the argument Dataset. 81 82 >>> import yt 83 >>> from yt.visualization.volume_rendering.api import Scene 84 >>> ds = yt.load("IsolatedGalaxy/galaxy0030/galaxy0030") 85 >>> sc = Scene() 86 >>> cam = sc.add_camera(ds) 87 88 Here, we set the camera properties manually: 89 90 >>> import yt 91 >>> from yt.visualization.volume_rendering.api import Scene 92 >>> sc = Scene() 93 >>> cam = sc.add_camera() 94 >>> cam.position = np.array([0.5, 0.5, -1.0]) 95 >>> cam.focus = np.array([0.5, 0.5, 0.0]) 96 >>> cam.north_vector = np.array([1.0, 0.0, 0.0]) 97 98 Finally, we create a camera with a non-default lens: 99 100 >>> import yt 101 >>> from yt.visualization.volume_rendering.api import Scene 102 >>> ds = yt.load("IsolatedGalaxy/galaxy0030/galaxy0030") 103 >>> sc = Scene() 104 >>> cam = sc.add_camera(ds, lens_type="perspective") 105 106 """ 107 108 _moved = True 109 _width = None 110 _focus = None 111 _position = None 112 _resolution = None 113 114 def __init__(self, scene, data_source=None, lens_type="plane-parallel", auto=False): 115 # import this here to avoid an import cycle 116 from .scene import Scene 117 118 if not isinstance(scene, Scene): 119 raise RuntimeError( 120 "The first argument passed to the Camera initializer is a " 121 "%s object, expected a %s object" % (type(scene), Scene) 122 ) 123 self.scene = weakref.proxy(scene) 124 self.lens = None 125 self.north_vector = None 126 self.normal_vector = None 127 self.light = None 128 self.data_source = data_source_or_all(data_source) 129 self._resolution = (512, 512) 130 131 if self.data_source is not None: 132 self.scene._set_new_unit_registry(self.data_source.ds.unit_registry) 133 self._focus = self.data_source.ds.domain_center 134 self._position = self.data_source.ds.domain_right_edge 135 self._width = self.data_source.ds.arr( 136 [1.5 * self.data_source.ds.domain_width.max()] * 3 137 ) 138 self._domain_center = self.data_source.ds.domain_center 139 self._domain_width = self.data_source.ds.domain_width 140 else: 141 self._focus = scene.arr([0.0, 0.0, 0.0], "unitary") 142 self._width = scene.arr([1.0, 1.0, 1.0], "unitary") 143 self._position = scene.arr([1.0, 1.0, 1.0], "unitary") 144 145 if auto: 146 self.set_defaults_from_data_source(data_source) 147 148 super().__init__( 149 self.focus - self.position, self.north_vector, steady_north=False 150 ) 151 152 self.set_lens(lens_type) 153 154 def position(): 155 doc = """ 156 The location of the camera. 157 158 Parameters 159 ---------- 160 161 position : number, YTQuantity, :obj:`!iterable`, or 3 element YTArray 162 If a scalar, assumes that the position is the same in all three 163 coordinates. If an iterable, must contain only scalars or 164 (length, unit) tuples. 165 """ 166 167 def fget(self): 168 return self._position 169 170 def fset(self, value): 171 position = _sanitize_camera_property_units(value, self.scene) 172 if np.array_equal(position, self.focus): 173 raise RuntimeError( 174 "Cannot set the camera focus and position to the same value" 175 ) 176 self._position = position 177 self.switch_orientation( 178 normal_vector=self.focus - self._position, 179 north_vector=self.north_vector, 180 ) 181 182 def fdel(self): 183 del self._position 184 185 return locals() 186 187 position = property(**position()) 188 189 def width(): 190 doc = """The width of the region that will be seen in the image. 191 192 Parameters 193 ---------- 194 195 width : number, YTQuantity, :obj:`!iterable`, or 3 element YTArray 196 The width of the volume rendering in the horizontal, vertical, and 197 depth directions. If a scalar, assumes that the width is the same in 198 all three directions. If an iterable, must contain only scalars or 199 (length, unit) tuples. 200 """ 201 202 def fget(self): 203 return self._width 204 205 def fset(self, value): 206 width = _sanitize_camera_property_units(value, self.scene) 207 self._width = width 208 self.switch_orientation() 209 210 def fdel(self): 211 del self._width 212 self._width = None 213 214 return locals() 215 216 width = property(**width()) 217 218 def focus(): 219 doc = """ 220 The focus defines the point the Camera is pointed at. 221 222 Parameters 223 ---------- 224 225 focus : number, YTQuantity, :obj:`!iterable`, or 3 element YTArray 226 The width of the volume rendering in the horizontal, vertical, and 227 depth directions. If a scalar, assumes that the width is the same in 228 all three directions. If an iterable, must contain only scalars or 229 (length, unit) tuples. 230 """ 231 232 def fget(self): 233 return self._focus 234 235 def fset(self, value): 236 focus = _sanitize_camera_property_units(value, self.scene) 237 if np.array_equal(focus, self.position): 238 raise RuntimeError( 239 "Cannot set the camera focus and position to the same value" 240 ) 241 self._focus = focus 242 self.switch_orientation( 243 normal_vector=self.focus - self._position, north_vector=None 244 ) 245 246 def fdel(self): 247 del self._focus 248 249 return locals() 250 251 focus = property(**focus()) 252 253 def resolution(): 254 doc = """The resolution is the number of pixels in the image that 255 will be produced. Must be a 2-tuple of integers or an integer.""" 256 257 def fget(self): 258 return self._resolution 259 260 def fset(self, value): 261 if is_sequence(value): 262 if len(value) != 2: 263 raise RuntimeError 264 else: 265 value = (value, value) 266 self._resolution = value 267 268 def fdel(self): 269 del self._resolution 270 self._resolution = None 271 272 return locals() 273 274 resolution = property(**resolution()) 275 276 def set_resolution(self, resolution): 277 """ 278 The resolution is the number of pixels in the image that 279 will be produced. Must be a 2-tuple of integers or an integer. 280 """ 281 self.resolution = resolution 282 283 def get_resolution(self): 284 """ 285 Returns the resolution of the volume rendering 286 """ 287 return self.resolution 288 289 def _get_sampler_params(self, render_source): 290 lens_params = self.lens._get_sampler_params(self, render_source) 291 lens_params.update(width=self.width) 292 pos = self.position.in_units("code_length").d 293 width = self.width.in_units("code_length").d 294 lens_params.update(camera_data=np.vstack((pos, width, self.unit_vectors.d))) 295 return lens_params 296 297 def set_lens(self, lens_type): 298 r"""Set the lens to be used with this camera. 299 300 Parameters 301 ---------- 302 303 lens_type : string 304 Must be one of the following: 305 'plane-parallel' 306 'perspective' 307 'stereo-perspective' 308 'fisheye' 309 'spherical' 310 'stereo-spherical' 311 312 """ 313 if isinstance(lens_type, Lens): 314 self.lens = lens_type 315 elif lens_type not in lenses: 316 raise RuntimeError( 317 "Lens type %s not in available list of available lens " 318 "types (%s)" % (lens_type, list(lenses.keys())) 319 ) 320 else: 321 self.lens = lenses[lens_type]() 322 self.lens.set_camera(self) 323 324 def set_defaults_from_data_source(self, data_source): 325 """Resets the camera attributes to their default values""" 326 327 position = data_source.ds.domain_right_edge 328 329 width = 1.5 * data_source.ds.domain_width.max() 330 (xmi, xma), (ymi, yma), (zmi, zma) = data_source.quantities["Extrema"]( 331 ["x", "y", "z"] 332 ) 333 width = np.sqrt((xma - xmi) ** 2 + (yma - ymi) ** 2 + (zma - zmi) ** 2) 334 focus = data_source.get_field_parameter("center") 335 336 if is_sequence(width) and len(width) > 1 and isinstance(width[1], str): 337 width = data_source.ds.quan(width[0], units=width[1]) 338 # Now convert back to code length for subsequent manipulation 339 width = width.in_units("code_length") # .value 340 if not is_sequence(width): 341 width = data_source.ds.arr([width, width, width], units="code_length") 342 # left/right, top/bottom, front/back 343 if not isinstance(width, YTArray): 344 width = data_source.ds.arr(width, units="code_length") 345 if not isinstance(focus, YTArray): 346 focus = data_source.ds.arr(focus, units="code_length") 347 348 # We can't use the property setters yet, since they rely on attributes 349 # that will not be set up until the base class initializer is called. 350 # See Issue #1131. 351 self._width = width 352 self._focus = focus 353 self._position = position 354 self._domain_center = data_source.ds.domain_center 355 self._domain_width = data_source.ds.domain_width 356 357 super().__init__( 358 self.focus - self.position, self.north_vector, steady_north=False 359 ) 360 self._moved = True 361 362 def set_width(self, width): 363 r"""Set the width of the image that will be produced by this camera. 364 365 Parameters 366 ---------- 367 368 width : number, YTQuantity, :obj:`!iterable`, or 3 element YTArray 369 The width of the volume rendering in the horizontal, vertical, and 370 depth directions. If a scalar, assumes that the width is the same in 371 all three directions. If an iterable, must contain only scalars or 372 (length, unit) tuples. 373 """ 374 self.width = width 375 self.switch_orientation() 376 377 def get_width(self): 378 """Return the current camera width""" 379 return self.width 380 381 def set_position(self, position, north_vector=None): 382 r"""Set the position of the camera. 383 384 Parameters 385 ---------- 386 387 position : number, YTQuantity, :obj:`!iterable`, or 3 element YTArray 388 If a scalar, assumes that the position is the same in all three 389 coordinates. If an iterable, must contain only scalars or 390 (length, unit) tuples. 391 392 north_vector : array_like, optional 393 The 'up' direction for the plane of rays. If not specific, 394 calculated automatically. 395 396 """ 397 if north_vector is not None: 398 self.north_vector = north_vector 399 self.position = position 400 401 def get_position(self): 402 """Return the current camera position""" 403 return self.position 404 405 def set_focus(self, new_focus): 406 """Sets the point the Camera is pointed at. 407 408 Parameters 409 ---------- 410 411 new_focus : number, YTQuantity, :obj:`!iterable`, or 3 element YTArray 412 If a scalar, assumes that the focus is the same is all three 413 coordinates. If an iterable, must contain only scalars or 414 (length, unit) tuples. 415 416 """ 417 self.focus = new_focus 418 419 def get_focus(self): 420 """Returns the current camera focus""" 421 return self.focus 422 423 def switch_orientation(self, normal_vector=None, north_vector=None): 424 r"""Change the view direction based on any of the orientation parameters. 425 426 This will recalculate all the necessary vectors and vector planes 427 related to an orientable object. 428 429 Parameters 430 ---------- 431 normal_vector: array_like, optional 432 The new looking vector from the camera to the focus. 433 north_vector : array_like, optional 434 The 'up' direction for the plane of rays. If not specific, 435 calculated automatically. 436 """ 437 if north_vector is None: 438 north_vector = self.north_vector 439 if normal_vector is None: 440 normal_vector = self.normal_vector 441 self._setup_normalized_vectors(normal_vector, north_vector) 442 self.lens.setup_box_properties(self) 443 444 def switch_view(self, normal_vector=None, north_vector=None): 445 r"""Change the view based on any of the view parameters. 446 447 This will recalculate the orientation and width based on any of 448 normal_vector, width, center, and north_vector. 449 450 Parameters 451 ---------- 452 normal_vector: array_like, optional 453 The new looking vector from the camera to the focus. 454 north_vector : array_like, optional 455 The 'up' direction for the plane of rays. If not specific, 456 calculated automatically. 457 """ 458 if north_vector is None: 459 north_vector = self.north_vector 460 if normal_vector is None: 461 normal_vector = self.normal_vector 462 self.switch_orientation(normal_vector=normal_vector, north_vector=north_vector) 463 self._moved = True 464 465 def rotate(self, theta, rot_vector=None, rot_center=None): 466 r"""Rotate by a given angle 467 468 Rotate the view. If `rot_vector` is None, rotation will occur 469 around the `north_vector`. 470 471 Parameters 472 ---------- 473 theta : float, in radians 474 Angle (in radians) by which to rotate the view. 475 rot_vector : array_like, optional 476 Specify the rotation vector around which rotation will 477 occur. Defaults to None, which sets rotation around 478 `north_vector` 479 rot_center : array_like, optional 480 Specify the center around which rotation will occur. Defaults 481 to None, which sets rotation around the original camera position 482 (i.e. the camera position does not change) 483 484 Examples 485 -------- 486 487 >>> import yt 488 >>> import numpy as np 489 >>> from yt.visualization.volume_rendering.api import Scene 490 >>> sc = Scene() 491 >>> cam = sc.add_camera() 492 >>> # rotate the camera by pi / 4 radians: 493 >>> cam.rotate(np.pi / 4.0) 494 >>> # rotate the camera about the y-axis instead of cam.north_vector: 495 >>> cam.rotate(np.pi / 4.0, np.array([0.0, 1.0, 0.0])) 496 >>> # rotate the camera about the origin instead of its own position: 497 >>> cam.rotate(np.pi / 4.0, rot_center=np.array([0.0, 0.0, 0.0])) 498 499 """ 500 rotate_all = rot_vector is not None 501 if rot_vector is None: 502 rot_vector = self.north_vector 503 if rot_center is None: 504 rot_center = self._position 505 rot_vector = ensure_numpy_array(rot_vector) 506 rot_vector = rot_vector / np.linalg.norm(rot_vector) 507 508 new_position = self._position - rot_center 509 R = get_rotation_matrix(theta, rot_vector) 510 new_position = np.dot(R, new_position) + rot_center 511 512 if (new_position == self._position).all(): 513 normal_vector = self.unit_vectors[2] 514 else: 515 normal_vector = rot_center - new_position 516 normal_vector = normal_vector / np.sqrt((normal_vector ** 2).sum()) 517 518 if rotate_all: 519 self.switch_view( 520 normal_vector=np.dot(R, normal_vector), 521 north_vector=np.dot(R, self.unit_vectors[1]), 522 ) 523 else: 524 self.switch_view(normal_vector=np.dot(R, normal_vector)) 525 if (new_position != self._position).any(): 526 self.set_position(new_position) 527 528 def pitch(self, theta, rot_center=None): 529 r"""Rotate by a given angle about the horizontal axis 530 531 Pitch the view. 532 533 Parameters 534 ---------- 535 theta : float, in radians 536 Angle (in radians) by which to pitch the view. 537 rot_center : array_like, optional 538 Specify the center around which rotation will occur. 539 540 Examples 541 -------- 542 543 >>> import yt 544 >>> import numpy as np 545 >>> from yt.visualization.volume_rendering.api import Scene 546 >>> sc = Scene() 547 >>> sc.add_camera() 548 >>> # pitch the camera by pi / 4 radians: 549 >>> cam.pitch(np.pi / 4.0) 550 >>> # pitch the camera about the origin instead of its own position: 551 >>> cam.pitch(np.pi / 4.0, rot_center=np.array([0.0, 0.0, 0.0])) 552 553 """ 554 self.rotate(theta, rot_vector=self.unit_vectors[0], rot_center=rot_center) 555 556 def yaw(self, theta, rot_center=None): 557 r"""Rotate by a given angle about the vertical axis 558 559 Yaw the view. 560 561 Parameters 562 ---------- 563 theta : float, in radians 564 Angle (in radians) by which to yaw the view. 565 rot_center : array_like, optional 566 Specify the center around which rotation will occur. 567 568 Examples 569 -------- 570 571 >>> import yt 572 >>> import numpy as np 573 >>> from yt.visualization.volume_rendering.api import Scene 574 >>> sc = Scene() 575 >>> cam = sc.add_camera() 576 >>> # yaw the camera by pi / 4 radians: 577 >>> cam.yaw(np.pi / 4.0) 578 >>> # yaw the camera about the origin instead of its own position: 579 >>> cam.yaw(np.pi / 4.0, rot_center=np.array([0.0, 0.0, 0.0])) 580 581 """ 582 self.rotate(theta, rot_vector=self.unit_vectors[1], rot_center=rot_center) 583 584 def roll(self, theta, rot_center=None): 585 r"""Rotate by a given angle about the view normal axis 586 587 Roll the view. 588 589 Parameters 590 ---------- 591 theta : float, in radians 592 Angle (in radians) by which to roll the view. 593 rot_center : array_like, optional 594 Specify the center around which rotation will occur. 595 596 Examples 597 -------- 598 599 >>> import yt 600 >>> import numpy as np 601 >>> from yt.visualization.volume_rendering.api import Scene 602 >>> sc = Scene() 603 >>> cam = sc.add_camera(ds) 604 >>> # roll the camera by pi / 4 radians: 605 >>> cam.roll(np.pi / 4.0) 606 >>> # roll the camera about the origin instead of its own position: 607 >>> cam.roll(np.pi / 4.0, rot_center=np.array([0.0, 0.0, 0.0])) 608 609 """ 610 self.rotate(theta, rot_vector=self.unit_vectors[2], rot_center=rot_center) 611 612 def iter_rotate(self, theta, n_steps, rot_vector=None, rot_center=None): 613 r"""Loop over rotate, creating a rotation 614 615 This will rotate `n_steps` until the current view has been 616 rotated by an angle `theta`. 617 618 Parameters 619 ---------- 620 theta : float, in radians 621 Angle (in radians) by which to rotate the view. 622 n_steps : int 623 The number of snapshots to make. 624 rot_vector : array_like, optional 625 Specify the rotation vector around which rotation will 626 occur. Defaults to None, which sets rotation around the 627 original `north_vector` 628 rot_center : array_like, optional 629 Specify the center around which rotation will occur. Defaults 630 to None, which sets rotation around the original camera position 631 (i.e. the camera position does not change) 632 633 Examples 634 -------- 635 636 >>> import yt 637 >>> import numpy as np 638 >>> ds = yt.load("IsolatedGalaxy/galaxy0030/galaxy0030") 639 640 >>> im, sc = yt.volume_render(ds) 641 >>> cam = sc.camera 642 >>> for i in cam.iter_rotate(np.pi, 10): 643 ... im = sc.render() 644 ... sc.save("rotation_%04i.png" % i) 645 646 """ 647 648 dtheta = (1.0 * theta) / n_steps 649 for i in range(n_steps): 650 self.rotate(dtheta, rot_vector=rot_vector, rot_center=rot_center) 651 yield i 652 653 def iter_move(self, final, n_steps, exponential=False): 654 r"""Loop over an iter_move and return snapshots along the way. 655 656 This will yield `n_steps` until the current view has been 657 moved to a final center of `final`. 658 659 Parameters 660 ---------- 661 final : YTArray 662 The final center to move to after `n_steps` 663 n_steps : int 664 The number of snapshots to make. 665 exponential : boolean 666 Specifies whether the move/zoom transition follows an 667 exponential path toward the destination or linear. 668 Default is False. 669 670 Examples 671 -------- 672 673 >>> import yt 674 >>> import numpy as np 675 >>> ds = yt.load("IsolatedGalaxy/galaxy0030/galaxy0030") 676 >>> final_position = ds.arr([0.2, 0.3, 0.6], "unitary") 677 >>> im, sc = yt.volume_render(ds) 678 >>> cam = sc.camera 679 >>> for i in cam.iter_move(final_position, 10): 680 ... sc.render() 681 ... sc.save("move_%04i.png" % i) 682 683 """ 684 assert isinstance(final, YTArray) 685 if exponential: 686 position_diff = (final / self.position) * 1.0 687 dx = position_diff ** (1.0 / n_steps) 688 else: 689 dx = (final - self.position) * 1.0 / n_steps 690 for i in range(n_steps): 691 if exponential: 692 self.set_position(self.position * dx) 693 else: 694 self.set_position(self.position + dx) 695 yield i 696 697 def zoom(self, factor): 698 r"""Change the width of the FOV of the camera. 699 700 This will appear to zoom the camera in by some `factor` toward the 701 focal point along the current view direction, but really it's just 702 changing the width of the field of view. 703 704 Parameters 705 ---------- 706 factor : float 707 The factor by which to divide the width 708 709 Examples 710 -------- 711 712 >>> import yt 713 >>> from yt.visualization.volume_rendering.api import Scene 714 >>> ds = yt.load("IsolatedGalaxy/galaxy0030/galaxy0030") 715 >>> sc = Scene() 716 >>> cam = sc.add_camera(ds) 717 >>> cam.zoom(1.1) 718 719 """ 720 721 self.width[:2] = self.width[:2] / factor 722 723 def iter_zoom(self, final, n_steps): 724 r"""Loop over a iter_zoom and return snapshots along the way. 725 726 This will yield `n_steps` snapshots until the current view has been 727 zooming in to a final factor of `final`. 728 729 Parameters 730 ---------- 731 final : float 732 The zoom factor, with respect to current, desired at the end of the 733 sequence. 734 n_steps : int 735 The number of zoom snapshots to make. 736 737 Examples 738 -------- 739 740 >>> import yt 741 >>> ds = yt.load("IsolatedGalaxy/galaxy0030/galaxy0030") 742 >>> im, sc = yt.volume_render(ds) 743 >>> cam = sc.camera 744 >>> for i in cam.iter_zoom(100.0, 10): 745 ... sc.render() 746 ... sc.save("zoom_%04i.png" % i) 747 748 """ 749 f = final ** (1.0 / n_steps) 750 for i in range(n_steps): 751 self.zoom(f) 752 yield i 753 754 def __repr__(self): 755 disp = ( 756 "<Camera Object>:\n\tposition:%s\n\tfocus:%s\n\t" 757 + "north_vector:%s\n\twidth:%s\n\tlight:%s\n\tresolution:%s\n" 758 ) % ( 759 self.position, 760 self.focus, 761 self.north_vector, 762 self.width, 763 self.light, 764 self.resolution, 765 ) 766 disp += f"Lens: {self.lens}" 767 return disp 768