text: # Copyright (c) 2016-2021 The Regents of the University of Michigan
  # Part of fresnel, released under the BSD 3-Clause License.
  
  """Cameras."""
  
  import numpy
  import math
  
  from . import _common
  
  
  class Camera(object):
      """Camera base class.
  
      A `Camera` defines the view into the `Scene`.
  
      .. image:: camera.svg
  
      `Camera` space is a coordinate system centered on the camera's position.
      Positive *x* points to the right in the image, positive *y* points up, and
      positive *z* points out of the screen. The visible area in the image plane
      is centered on `look_at` with the given `height`. The visible width is
      ``height * aspect`` where *aspect* is the aspect ratio determined by the
      resolution of the image in `Tracer` (``aspect = tracer.w / tracer.h``).
      `Camera` space shares units with `Scene` space.
  
      `Camera` provides common methods and properties for all camera
      implementations. `Camera` cannot be used directly, use one of the
      subclasses.
  
      See Also:
          * `Orthographic`
          * `Perspective`
      """
  
      def __init__(self, _camera):
          # The Python level `Camera` class keeps a reference to a C++
          # `UserCamera` class up to date with all parameter changes.
          self._camera = _camera
  
      def __copy__(self):
          """Make a copy of this camera."""
          cam = _common.UserCamera()
          cam.model = self._camera.model
          cam.position = self._camera.position
          cam.look_at = self._camera.look_at
          cam.up = self._camera.up
          cam.h = self._camera.h
          cam.f = self._camera.f
          cam.f_stop = self._camera.f_stop
          cam.focus_distance = self._camera.focus_distance
          return _from_cpp(cam)
  
      @property
      def position(self):
          """((3, ) `numpy.ndarray` of ``numpy.float32``): Camera position."""
          return numpy.array([
              self._camera.position.x, self._camera.position.y,
              self._camera.position.z
          ],
                             dtype=numpy.float32)
  
      @position.setter
      def position(self, value):
          if len(value) != 3:
              raise ValueError("position must have length 3")
          self._camera.position = _common.vec3f(*value)
  
      @property
      def look_at(self):
          """((3, ) `numpy.ndarray` of ``numpy.float32``): The point the camera\
              looks at.
  
          ``position - look_at`` defines the *+z* direction in camera space.
          """
          return numpy.array([
              self._camera.look_at.x, self._camera.look_at.y,
              self._camera.look_at.z
          ],
                             dtype=numpy.float32)
  
      @look_at.setter
      def look_at(self, value):
          if len(value) != 3:
              raise ValueError("look_at must have length 3")
          self._camera.look_at = _common.vec3f(*value)
  
      @property
      def up(self):
          """((3, ) `numpy.ndarray` of ``numpy.float32``): A vector pointing\
              toward the *+y* direction in camera space.
  
          The component of `up` perpendicular to ``look_at - position`` defines
          the *+y* direction in camera space.
          """
          return numpy.array(
              [self._camera.up.x, self._camera.up.y, self._camera.up.z],
              dtype=numpy.float32)
  
      @up.setter
      def up(self, value):
          if len(value) != 3:
              raise ValueError("up must have length 3")
          self._camera.up = _common.vec3f(*value)
  
      @property
      def height(self):
          """float: The height of the image plane."""
          return self._camera.h
  
      @height.setter
      def height(self, value):
          self._camera.h = float(value)
  
      @property
      def basis(self):
          """((3, 3) `numpy.ndarray` of ``numpy.float32``): Orthonormal camera\
              basis.
  
          `basis` is computed from `position <Camera.position>`, `look_at`, and
          `up`. The 3 vectors of the basis define the *+x*, *+y*, and *+z* camera
          space directions in scene space.
          """
          b = _common.CameraBasis(self._camera)
          return numpy.array([(b.u.x, b.u.y, b.u.z), (b.v.x, b.v.y, b.v.z),
                              (b.w.x, b.w.y, b.w.z)],
                             dtype=numpy.float32)
  
  
  class Orthographic(Camera):
      """Orthographic camera.
  
      Args:
          position ((3, ) `numpy.ndarray` of ``numpy.float32``): Camera position.
  
          look_at ((3, ) `numpy.ndarray` of ``numpy.float32``): The
              point the camera looks at (the center of the focal plane).
  
          up ((3, ) `numpy.ndarray` of ``numpy.float32``): A vector pointing
              toward the *+y* direction in camera space.
  
          height (float): The height of the image plane.
  
      An orthographic camera traces parallel rays from the image plane into the
      scene. Lines that are parallel in the `Scene` will remain parallel in the
      rendered image.
  
      .. image:: orthographic.svg
  
      `position <Camera.position>` is the center of the image plane in `Scene`
      space. `look_at` is the point in `Scene` space that will be in the center of
      the image. Together, these vectors define the image plane which is
      perpendicular to the line from `position <Camera.position>` to `look_at`.
  
      `up` is a vector in `Scene` space that defines the (*+y*) direction in the
      camera space). `up` does not need to be perpendicular to the line from
      *position* to *look_at*, but it must not be parallel to that line. `height`
      sets the height of the image sensor in `Scene` units. The width is ``height
      * aspect`` where *aspect* is the aspect ratio determined by the resolution
      of the image in `Tracer` (``aspect = tracer.w / tracer.h``).
  
      Note:
          Only objects inside the rectangular cuboid defined by corners of the
          image sensor and the focal plane (extended to infinite height) will
          appear in the image.
  
          Objects in front of the image plane will appear in the rendered image,
          objects behind the plane will not.
  
      Tip:
          Place the camera `position <Camera.position>` outside the geometry of
          the `Scene`. Decrease `height` to zoom in and increase `height` to zoom
          out.
      """
  
      def __init__(self, position, look_at, up, height):
          cam = _common.UserCamera()
          cam.model = _common.CameraModel.orthographic
  
          super().__init__(cam)
  
          self.position = position
          self.look_at = look_at
          self.up = up
          self.height = height
  
      def __repr__(self):
          """str: Representation of the camera."""
          s = "fresnel.camera.Orthographic("
          s += f"position={tuple(self.position)}, "
          s += f"look_at={tuple(self.look_at)}, "
          s += f"up={tuple(self.up)}, "
          s += f"height={self.height})"
          return s
  
      @classmethod
      def fit(cls, scene, view='auto', margin=0.05):
          """Fit a camera to a `Scene`.
  
          Create an orthographic camera that fits the entire height of the scene
          in the image plane.
  
          Args:
              scene (`Scene`): Fit the camera to this scene.
  
              view (str): Select view
  
              margin (float): Fraction of extra space to leave on the top and
                  bottom of the scene.
  
          *view* may be 'auto', 'isometric', or 'front'.
  
          The isometric view is an orthographic projection from a particular angle
          so that the x,y, and z directions are equal lengths. The front view is
          an orthographic projection where +x points to the right, +y points up
          and +z points out of the screen in the image plane. 'auto' automatically
          selects 'isometric' for 3D scenes and 'front' for 2D scenes.
          """
          vectors = {
              'front':
                  dict(v=numpy.array([0, 0, 1]),
                       up=numpy.array([0, 1, 0]),
                       right=numpy.array([1, 0, 0])),
              'isometric':
                  dict(v=numpy.array([1, 1, 1]) / math.sqrt(3),
                       up=numpy.array([-1, 2, -1]) / math.sqrt(6),
                       right=numpy.array([1, 0, -1]) / math.sqrt(2))
          }
  
          # raise error if the scene is empty
          if len(scene.geometry) == 0:
              raise ValueError('The camera cannot be fit because the scene has no'
                               ' geometries. Add geometries to the scene before'
                               ' calling fit.')
  
          # find the center of the scene
          extents = scene.get_extents()
  
          # choose an appropriate view automatically
          if view == 'auto':
              xw = extents[1, 0] - extents[0, 0]
              yw = extents[1, 1] - extents[0, 1]
              zw = extents[1, 2] - extents[0, 2]
  
              if zw < 0.51 * max(xw, yw):
                  view = 'front'
              else:
                  view = 'isometric'
  
          v = vectors[view]['v']
          up = vectors[view]['up']
  
          # make a list of points of the cube surrounding the scene
          points = numpy.array([[extents[0, 0], extents[0, 1], extents[0, 2]],
                                [extents[0, 0], extents[0, 1], extents[1, 2]],
                                [extents[0, 0], extents[1, 1], extents[0, 2]],
                                [extents[0, 0], extents[1, 1], extents[1, 2]],
                                [extents[1, 0], extents[0, 1], extents[0, 2]],
                                [extents[1, 0], extents[0, 1], extents[1, 2]],
                                [extents[1, 0], extents[1, 1], extents[0, 2]],
                                [extents[1, 0], extents[1, 1], extents[1, 2]]])
  
          # find the center of the box
          center = (extents[0, :] + extents[1, :]) / 2
          points = points - center
  
          # determine the extent of the scene box in the up direction
          up_projection = numpy.dot(points, up)
          height = (1 + margin) * numpy.max(numpy.abs(up_projection)) * 2
  
          # determine the extent of the scene box in the view direction
          view_projection = numpy.dot(points, v)
          view_distance = numpy.max(view_projection) * 1.10
  
          # build the camera
          return cls(position=center + view_distance * v,
                     look_at=center,
                     up=up,
                     height=height)
  
  
  class Perspective(Camera):
      """Perspective camera.
  
      Args:
          position ((3, ) `numpy.ndarray` of ``numpy.float32``): Camera position.
  
          look_at ((3, ) `numpy.ndarray` of ``numpy.float32``): The
              point the camera looks at (the center of the focal plane).
  
          up ((3, ) `numpy.ndarray` of ``numpy.float32``): A vector pointing
              toward the *+y* direction in camera space.
  
          focal_length (float): Focal length of the camera lens.
  
          focus_distance (float): Distance to the focal plane.
  
          f_stop (float): F-stop ratio for the lens.
  
          height (float): The height of the image plane.
  
      A perspective camera traces diverging rays from the camera position
      through the image plane into the scene. Lines that are parallel in the
      `Scene` will converge rendered image.
  
      .. image:: perspective.svg
  
      `position <Camera.position>` is the center of projection `Scene`
      space. `look_at` is the point in `Scene` space that will be in the center of
      the image. Together, these vectors define the image plane which is
      perpendicular to the line from `position <Camera.position>` to `look_at`.
  
      `up` is a vector in `Scene` space that defines the (*+y*) direction in the
      camera space). `up` does not need to be perpendicular to the line from
      *position* to *look_at*, but it must not be parallel to that line.
  
      Note:
          Only objects inside the rectangular pyramid defined by the position and
          corners of the image sensor (extended to infinite height) will appear in
          the image.
  
      `Perspective` models an ideal camera system with a sensor and a thin lens.
      The sensor lies in the image plane and is the location where the pixels in
      the rendered image will be captured. `height` sets the height of the sensor
      in `Scene` units. The width is ``height * aspect`` where *aspect* is the
      aspect ratio determined by the resolution of the image in `Tracer` (``aspect
      = tracer.w / tracer.h``). `focal_length` sets the distance between `position
      <Camera.position>` and the image plane.
  
      Note:
          The camera `height` should be small relative to the objects in the
          `Scene` with those objects in front of the image plane. If the scene
          units are decimeters, the default `height` of 0.24 is 24 mm, the height
          of a 35 mm camera sensor.
  
      Tip:
          There are two ways to zoom a perspective camera. 1) Move the position
          of the camera while keeping the focal length fixed. Photographers call
          this "zooming with your feet" and it maintains a fixed field of view.
          2) Increase the `focal_length` to zoom in or decrease it to zoom out
          while keeping position fixed. This is the the equivalent of rotating
          the focal length setting on a zoom lens. Changing `focal_length`
          changes the field of view.
  
      Like a digital camera, the `Perspective` camera must be focused. The focal
      plane is parallel to the image plane at a distance `focus_distance` from the
      camera `position <Camera.position>`. Objects on the focal plane will be in
      sharp focus. Objects in front of and behind the plane will be out of focus.
      Out of focus areas in an image are called **bokeh** and can be used to draw
      the viewer's attention to the subject that is in clear focus. The space in
      front of and behind the focal plane that appears to be in focus is the
      **depth of field**. Set `f_stop` to control the amount of depth of field.
      Small, non-zero values will lead to very little depth of field and a value
      of *inf* will extend the depth of field to infinity.
  
      Note:
          There are convenience methods to set the camera parameters:
  
          * `focus_on` takes a point and computes the `focus_distance` to put that
            point on the focal plane.
          * `depth_of_field` computes the `f_stop` needed to achieve a given
            depth of field.
          * `vertical_field_of_view` computes the `focal_length` needed to achieve
            a given field of view angle.
  
      Tip:
          The default `height` of 0.24 works well for scene objects that are size
          ~1 or larger. If the typical objects in your scene are much smaller,
          adjust `height` by an appropriate fraction.
  
      """
  
      def __init__(self,
                   position,
                   look_at,
                   up,
                   focal_length=.5,
                   focus_distance=10,
                   f_stop=math.inf,
                   height=0.24):
          cam = _common.UserCamera()
          cam.model = _common.CameraModel.perspective
  
          super().__init__(cam)
  
          self.position = position
          self.look_at = look_at
          self.up = up
          self.focus_distance = focus_distance
          self.focal_length = focal_length
          self.f_stop = f_stop
          self.height = height
  
      def __repr__(self):
          """str: Representation of the camera."""
          s = "fresnel.camera.Perspective("
          s += f"position={tuple(self.position)}, "
          s += f"look_at={tuple(self.look_at)}, "
          s += f"up={tuple(self.up)}, "
          s += f"focus_distance={self.focus_distance}, "
          s += f"focal_length={self.focal_length}, "
          s += f"f_stop={self.f_stop}, "
          s += f"height={self.height})"
          return s
  
      @property
      def focal_length(self):
          """float: Focal length of the camera lens.
  
          The focal length relative to the image `height` sets the field of view.
          Given a fixed `height`, a larger `focal_length` gives a narrower field
          of view.
  
          Tip:
              With the default height of 0.24, typical focal lengths range from
              .18 (wide angle) to 0.5 (normal) to 6.0 (telephoto).
  
          See Also:
              `vertical_field_of_view`
          """
          return self._camera.f
  
      @focal_length.setter
      def focal_length(self, value):
          self._camera.f = value
  
      @property
      def f_stop(self):
          """float: F-stop ratio for the lens.
  
          Set the aperture of the opening into the lens in f-stops. This sets the
          range of the scene that is in sharp focus. Smaller values of `f_stop`
          result in more background blur.
  
          Tip:
              Use `depth_of_field` to set the range of sharp focus in `Scene`
              distance units.
          """
          return self._camera.f_stop
  
      @f_stop.setter
      def f_stop(self, value):
          self._camera.f_stop = value
  
      @property
      def focus_distance(self):
          """float: Distance to the focal plane.
  
          The focus distance is the distance from the camera position to
          the center of focal plane.
  
          Tip:
              Use `focus_on` to compute the focus distance to a particular point
              in the `Scene`.
          """
          return self._camera.focus_distance
  
      @focus_distance.setter
      def focus_distance(self, value):
          self._camera.focus_distance = value
  
      @property
      def depth_of_field(self):
          """float: The distance about the focal plane in sharp focus.
  
          The area of sharp focus extends in front and behind the focal plane. The
          distance between the front and back areas of sharp focus is the depth
          of field.
  
          The depth of field is a function of `focus_distance`, `focal_length`,
          `f_stop`, and `height`.
  
          Setting `depth_of_field` computes `f_stop` to obtain the desired depth
          of field as a function of `focus_distance`, `focal_length`, and
          `height`.
  
          Note:
              `depth_of_field` does not remain fixed after setting it.
          """
          f = self.focal_length
          N = self.f_stop
          s = self.focus_distance
  
          # c is the circle of confusion. A commonly accepted value for 35 mm
          # cameras is 0.03mm. 35mm film is 24mm high, so c = h/(0.03 mm / 24 mm)
          # => c = h/800
          c = self.height / 800
  
          H = f**2 / (N * c)
  
          # the depth of field is infinite when focusing past the hyperfocal
          # distance
          if H - (s - f) <= 0 or H + (s - f) <= 0:
              return math.inf
  
          return s * H * (1 / (H - (s - f)) - 1 / (H + (s - f)))
  
      @depth_of_field.setter
      def depth_of_field(self, value):
          f = self.focal_length
          d = value
          s = self.focus_distance
  
          # c is the circle of confusion. A commonly accepted value for 35 mm
          # cameras is 0.03mm. 35mm film is 24mm high, so c = h/(0.03 mm / 24 mm)
          # => c = h/800
          c = self.height / 800
  
          a = math.sqrt(c**2 * f**4 * (d**2 + s**2) * (f - s)**2)
          b = c * f**2 * s * (f - s)
          numerator = a + b
          denominator = c**2 * d * (f - s)**2
          self.f_stop = numerator / denominator
  
      @property
      def focus_on(self):
          """(3, ) `numpy.ndarray` of ``numpy.float32``): A point in the focal\
              plane.
  
          The area of sharp focus extends in front and behind the focal plane.
  
          The focal plane is a function of `focus_distance`,
          `position <Camera.position>`, and `look_at`.
  
          Setting `focus_on` computes `focus_distance` so that the given point
          is on the focal plane.
  
          Note:
              `focus_on` does not remain fixed after setting it.
          """
          d = -self.basis[2, :]
          return self.position + d * self.focus_distance
  
      @focus_on.setter
      def focus_on(self, value):
          if len(value) != 3:
              raise ValueError("focus_on must have length 3")
  
          d = -self.basis[2, :]
          self.focus_distance = numpy.dot(d, value - self.position)
  
      @property
      def vertical_field_of_view(self):
          """float: Vertical field of view.
  
          The vertical field of view is the angle (in radians) that the camera
          covers in the +y direction. It is a function of `focal_length`
          and `height`.
  
          Setting `vertical_field_of_view` computes `focal_length` to achieve
          the given field of view.
  
          Note:
              `vertical_field_of_view` does not remain fixed after setting it.
          """
          return 2 * math.atan(self.height / (2 * self.focal_length))
  
      @vertical_field_of_view.setter
      def vertical_field_of_view(self, value):
          self.focal_length = self.height / (2 * math.tan(value / 2))
  
  
  def _from_cpp(cam):
      """Make a Python camera object from a C++ UserCamera.
  
      There is only one UserCamera class with mode flags at the C++ level while
      we expose them as separate classes at the Python level.
      """
      if cam.model == _common.CameraModel.orthographic:
          result = Orthographic(position=(0, 0, 0),
                                look_at=(0, 0, 1),
                                up=(0, 1, 0),
                                height=1)
          result._camera = cam
      elif cam.model == _common.CameraModel.perspective:
          result = Perspective(position=(0, 0, 0),
                               look_at=(0, 0, 1),
                               up=(0, 1, 0),
                               focus_distance=1)
          result._camera = cam
      else:
          raise RuntimeError("Invalid camera model")
  
      return result
  