"""A VTK camera object."""
import vtk
import math
from pathlib import Path
from typing import Tuple, List, Union, TypeVar, Type
from honeybee_radiance.view import View
from ladybug_geometry.geometry3d import Point3D, LineSegment3D
T = TypeVar('T', bound='Camera')
def _get_focal_point(focal_point: Union[Tuple[float, float, float], None],
position: Tuple[float, float, float],
direction: Tuple[float, float, float]) -> Tuple[float, float, float]:
"""Set the focal point of the camera.
Args:
focal_point: x, y, z coordinates of the focal point. If not set, the focal
point will be set by moving the point of camera position in the direction
of the camera direction.
position: x, y, z coordinates of the camera position.
direction: x, y, z components of the camera direction.
Returns:
x, y, z coordinates of the focal point.
"""
if not focal_point:
return (position[0] + direction[0], position[1] + direction[1], position[2] +
direction[2])
return focal_point
def _apply_projection(camera: Type[T], projection: str, parallel_scale: int,
clipping_range: Tuple[float, float]) -> vtk.vtkCamera:
"""Set the parallel projection camera.
Args:
camera: A VTK camera object.
projection: The projection type. 'v' or 'l'
parallel_scale: The parallel scale of the camera.
clipping_range: The clipping range of the camera.
Returns:
A VTK camera object.
"""
if projection == 'l':
camera.ParallelProjectionOn()
if parallel_scale:
camera.SetParallelScale(parallel_scale)
if clipping_range:
camera.SetClippingRange(clipping_range)
return camera
return camera
[docs]class Camera(View):
"""Create a vtk camera object.
This object inherits from the Honeybee_radiance.View object.
https://www.ladybug.tools/honeybee-radiance/docs/honeybee_radiance.view.html
Args:
identifier: A unique name for the camera. Defaults to 'camera'.
position: x, y, z coordinates of the camera in a 3D space. Defaults to (0, 0, 100)
which puts the camera 100 meters off of the XY plane (ground).
direction: x, y, and z components of a vector that represents the view direction
(aim) of the camera. Defaults to (0, 0, -1). Which means the camera will look
towards the XY plane (ground).
up_vector: x, y, and z component of the vector that represents where the top
of the camera is. Defaults to (0, 1, 0).
view_angle: The angular hight of the camera view in degrees. You can think of
this as the vertical view angle. Defaults to 60
projection: Choose between a perspective and parallel view type. 'v' will set
the perspective view and 'l' will set the parallel view. Defaults to 'v'.
reset_camera: A boolean that indicates whether the camera should be reset.
Resetting the camera is helpful when you want to capture an image of a
model from outside the model. This will make sure that the camera is far
away from the model and the whole model is captured. This should be set
to false in case of the intention is to take snapshots inside the model.
A use case is taking the snapshots of the grids in the model.
Defaults to True.
focal_point: x, y, and z coordinates of the point where the camera is looking at.
Defaults to None which means the camera will look towards the XY plane
(ground).
clipping_range: A range of two numbers that define the near plane and the far
plane respectively. Both of these planes are perpendicular to the camera
direction and are effective only in when the view type is parallel. The
distance from the camera to the near plane is the closest distance that
an object can be to the camera and still remain in the view. The distance
from the camera to the far plane is the farthest distance that an object
can be from the camera and still remain in the view. Defaults to None.
parallel_scale: Set the parallel scale for the camera. Note, that this parameters
works as an inverse scale. So larger numbers produce smaller images.This can
be thought of as the zoom in and zoom out control. This parameter is effective
only when the view type is parallel. Defaults to None.
"""
def __init__(self, identifier: str = 'camera',
position: Tuple[float, float, float] = (0, 0, 100),
direction: Tuple[float, float, float] = (0, 0, -1),
up_vector: Tuple[float, float, float] = (0, 1, 0),
view_angle: int = 60,
projection: str = 'v',
reset_camera: bool = True,
focal_point: Union[Tuple[float, float, float], None] = None,
clipping_range: Union[Tuple[float, float], None] = None,
parallel_scale: Union[int, None] = None) -> None:
super().__init__(
identifier=identifier, position=position, direction=direction,
up_vector=up_vector, h_size=view_angle, type=projection)
self._view_angle = view_angle
self._projection = projection
self._reset_camera = reset_camera
self._focal_point = focal_point
self._clipping_range = clipping_range
self._parallel_scale = parallel_scale
@property
def reset_camera(self) -> bool:
"""Get a boolean that indicates whether the camera should be reset."""
return self._reset_camera
@reset_camera.setter
def reset_camera(self, reset_camera: bool) -> None:
"""Set a boolean that indicates whether the camera should be reset. This is
helpful in case of the intention is to take snapshots inside the model.
"""
self._reset_camera = reset_camera
[docs] def to_vtk(self) -> vtk.vtkCamera:
"""Get a vtk camera object"""
camera = vtk.vtkCamera()
camera.SetPosition(self.position)
camera.ComputeViewPlaneNormal()
camera.SetViewUp(self.up_vector)
camera.SetViewAngle(self._view_angle)
camera.SetFocalPoint(_get_focal_point(self._focal_point, self.position,
self.direction))
camera = _apply_projection(camera, self._projection, self._parallel_scale,
self._clipping_range)
camera.OrthogonalizeViewUp()
return camera
[docs] @classmethod
def from_view(cls: Type[T], view: View) -> T:
"""Create a Camera object from a radiance view.
Args:
view: A radiance view
Returns:
A Camera object.
"""
return cls(identifier=view.identifier, position=view.position,
direction=view.direction, up_vector=view.up_vector,
view_angle=view.h_size if view.h_size > view.v_size else view.v_size,
projection=view.type)
[docs] @classmethod
def from_view_file(cls: Type[T], file_path: str) -> T:
"""Create a Camera object from a radiance view file.
Args:
file_path: A valid path to a radiance view file with .vf extension.
Returns:
A Camera object.
"""
view_file = Path(file_path)
if view_file.is_file() and view_file.as_posix()[-3:] == '.vf':
return cls.from_view(view=View.from_file(view_file.as_posix()))
else:
raise FileNotFoundError(
'Radiance view file not found.'
)
[docs] @classmethod
def aerial_cameras(cls: Type[T], bounds: List[Point3D], centroid: Point3D) -> List[T]:
"""Get four aerial cameras.
Args:
bounds: A list of Point3D objects representing bounds of the actors in the
scene.
centroid: A Point3D object representing the centroid of the actors.
Returns:
A list of Camera objects.
"""
# find the top most z-cordinate in the model
cord_point = {point.z: point for point in bounds}
topmost_z_cord = cord_point[sorted(cord_point, reverse=True)[0]].z
# move centroid to the level of top most z-cordinate
centroid_moved = Point3D(centroid.x, centroid.y,
topmost_z_cord)
# distance of four cameras from centroid
distances = [centroid.distance_to_point(pt) for pt in bounds]
farthest_distance = sorted(distances, reverse=True)
camera_distance = farthest_distance[0]
# generate total four points at 45 degrees and -45 degrees on left and right
# side of the centroid
pt0 = Point3D(
centroid_moved.x + math.cos(math.radians(45))*camera_distance,
centroid_moved.y + math.sin(math.radians(45))*camera_distance,
centroid_moved.z)
pt1 = Point3D(
centroid_moved.x + math.cos(math.radians(-45))*camera_distance,
centroid_moved.y + math.sin(math.radians(-45))*camera_distance,
centroid_moved.z)
pt2 = Point3D(
centroid_moved.x + math.cos(math.radians(45))*camera_distance*-1,
centroid_moved.y + math.sin(math.radians(45))*camera_distance*-1,
centroid_moved.z)
pt3 = Point3D(
centroid_moved.x + math.cos(math.radians(-45))*camera_distance*-1,
centroid_moved.y + math.sin(math.radians(-45))*camera_distance*-1,
centroid_moved.z)
camera_points = [pt0, pt3, pt2, pt1]
# get directions (vectors) from each point to the centroid
directions = [LineSegment3D.from_end_points(
pt, centroid).v for pt in camera_points]
# default camera identifiers
names = ['45_degrees', '315_degrees', '225_degrees', '135_degrees']
# create cameras from four points. These cameras look at the centroid.
default_cameras = []
for i in range(len(camera_points)):
point = camera_points[i]
direction = directions[i]
default_cameras.append(cls(identifier=names[i], position=(point.x, point.y, point.z),
direction=(direction.x, direction.y, direction.z),
up_vector=(0, 0, 1)))
return default_cameras