def to_viewport_point(self, point):
        with self.lock:
            if not isinstance(point, list) and not isinstance(point, tuple) \
                    and not isinstance(point, np.ndarray):
                raise GenericRolloutException("point must be a type of list, tuple, or numpy.ndarray")
            ray_origin = np.array(point)
            points_in_viewports = []
            for cam_pose, near_plane_info in zip(self.cam_poses, self.near_plane_infos):
                cam_pos = cam_pose["position"]
                cam_quaternion = cam_pose["orientation"]

                near_width = near_plane_info["width"]
                near_height = near_plane_info["height"]
                near_center = near_plane_info["position"]
                near_normal = near_plane_info["normal"]
                near_offset = near_plane_info["offset"]

                ray_dir = normalize(cam_pos - ray_origin)

                point_on_plane = ray_plane_intersect(ray_origin=ray_origin,
                                                     ray_dir=ray_dir,
                                                     plane_normal=near_normal,
                                                     plane_offset=near_offset,
                                                     threshold=self.threshold)
                if point_on_plane is None:
                    points_in_viewports.append((-1.0, -1.0))
                else:
                    point_in_viewport = project_to_2d(point_on_plane=point_on_plane,
                                                      plane_center=near_center,
                                                      plane_width=near_width,
                                                      plane_height=near_height,
                                                      plane_quaternion=cam_quaternion)
                    points_in_viewports.append(point_in_viewport)
            return points_in_viewports
    def _calculate_frustum_planes(self, cam_pos, cam_quaternion):
        cam_pos = np.array(cam_pos)
        cam_quaternion = np.array(cam_quaternion)
        cam_forward = Frustum.get_forward_vec(cam_quaternion)
        cam_up = Frustum.get_up_vec(cam_quaternion)
        cam_right = Frustum.get_right_vec(cam_quaternion)

        near_center = cam_pos + cam_forward * self.near
        far_center = cam_pos + cam_forward * self.far

        near_width = 2.0 * math.tan(self.horizontal_fov * 0.5) * self.near
        near_height = near_width * self.view_ratio
        far_width = 2.0 * math.tan(self.horizontal_fov * 0.5) * self.far
        far_height = far_width * self.view_ratio

        far_top_left = far_center + cam_up * (far_height * 0.5) - cam_right * (far_width * 0.5)
        far_top_right = far_center + cam_up * (far_height * 0.5) + cam_right * (far_width * 0.5)
        far_bottom_left = far_center - cam_up * (far_height * 0.5) - cam_right * (far_width * 0.5)
        far_bottom_right = far_center - cam_up * (far_height * 0.5) + cam_right * (far_width * 0.5)

        near_top_left = near_center + cam_up * (near_height * 0.5) - cam_right * (near_width * 0.5)
        near_top_right = near_center + cam_up * (near_height * 0.5) + cam_right * (near_width * 0.5)
        near_bottom_left = near_center - cam_up * (near_height * 0.5) - cam_right * (near_width * 0.5)
        near_bottom_right = near_center - cam_up * (near_height * 0.5) + cam_right * (near_width * 0.5)
        planes = []

        # near plane
        if self.ccw:
            p0, p1, p2 = near_bottom_right, near_bottom_left, near_top_left
        else:
            p0, p1, p2 = near_top_left, near_bottom_left, near_bottom_right
        near_plane_normal = normalize(np.cross(p1 - p0, p2 - p1))
        near_plane_offset = np.dot(near_plane_normal, p0)
        planes.append((near_plane_normal, near_plane_offset))

        # far plane
        if self.ccw:
            p0, p1, p2 = far_bottom_right, far_top_right, far_top_left
        else:
            p0, p1, p2 = far_top_left, far_top_right, far_bottom_right
        far_plane_normal = normalize(np.cross(p1 - p0, p2 - p1))
        far_plane_offset = np.dot(far_plane_normal, p0)
        planes.append((far_plane_normal, far_plane_offset))

        # left plane
        if self.ccw:
            p0, p1, p2 = near_bottom_left, far_bottom_left, far_top_left
        else:
            p0, p1, p2 = far_top_left, far_bottom_left, near_bottom_left
        left_plane_normal = normalize(np.cross(p1 - p0, p2-p1))
        left_plane_offset = np.dot(left_plane_normal, p0)
        planes.append((left_plane_normal, left_plane_offset))

        # right plane
        if self.ccw:
            p0, p1, p2 = near_top_right, far_top_right, far_bottom_right
        else:
            p0, p1, p2 = far_bottom_right, far_top_right, near_top_right
        right_plane_normal = normalize(np.cross(p1 - p0, p2-p1))
        right_plane_offset = np.dot(right_plane_normal, p0)
        planes.append((right_plane_normal, right_plane_offset))

        # top plane
        if self.ccw:
            p0, p1, p2 = near_top_left, far_top_left, far_top_right
        else:
            p0, p1, p2 = far_top_right, far_top_left, near_top_left
        top_plane_normal = normalize(np.cross(p1 - p0, p2-p1))
        top_plane_offset = np.dot(top_plane_normal, p0)
        planes.append((top_plane_normal, top_plane_offset))

        # bottom plane
        if self.ccw:
            p0, p1, p2 = near_bottom_right, far_bottom_right, far_bottom_left
        else:
            p0, p1, p2 = far_bottom_left, far_bottom_right, near_bottom_right
        bottom_plane_normal = normalize(np.cross(p1 - p0, p2 - p1))
        bottom_plane_offset = np.dot(bottom_plane_normal, p0)
        planes.append((bottom_plane_normal, bottom_plane_offset))

        cam_pose = {
            "position": cam_pos,
            "orientation": cam_quaternion
        }

        near_plane_info = {
            "width": near_width,
            "height": near_height,
            "position": near_center,
            "bounding_box": {
                "bottom_left": near_bottom_left,
                "top_left": near_top_left,
                "top_right": near_top_right,
                "bottom_right": near_bottom_right,
            },
            "normal": near_plane_normal,
            "offset": near_plane_offset
        }

        return planes, cam_pose, near_plane_info