Source code for sionna.rt.camera

#
# SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
"""
Implements a camera for rendering of a scene.
A camera defines a viewpoint for rendering.
"""
from __future__ import annotations

import drjit as dr
import mitsuba as mi

from sionna import rt


[docs] class Camera: # pylint: disable=line-too-long r""" Camera defining a position and view direction for rendering a scene In its local coordinate system, a camera looks toward the positive X-axis with the positive Z-axis being the upward direction. :param position: Position :math:`(x,y,z)` [m] as three-dimensional vector :param orientation: Orientation :math:`(\alpha, \beta, \gamma)` specified through three angles corresponding to a 3D rotation as defined in :eq:`rotation`. This parameter is ignored if ``look_at`` is not `None`. :param look_at: A position or object to look at. If set to `None`, then ``orientation`` is used to orient the device. """ # The convention of Mitsuba for camera is Y as up and look toward Z+. # However, Sionna uses Z as up and looks toward X+, for consistency # with radio devices. # The following transform peforms a rotation to ensure Sionna's # convention. # Note: rotation angle is specified in degrees. mi_to_sionna = ( mi.Transform4f().rotate((0, 0, 1), 90.0) @ mi.Transform4f().rotate((1, 0, 0), 90.0) ) def __init__( self, position: mi.Point3f, orientation: mi.Point3f = (0.,0.,0.), look_at: rt.RadioDevice | rt.SceneObject | mi.Poin3f | None = None): # Keep track of the "to world" transform. # Initialized to identity. self._to_world = mi.Transform4f() if look_at is not None: if orientation != (0., 0., 0.): msg = "Cannot specify both ``orientation`` and ``look_at``" raise ValueError(msg) # Set the position before the look_at so that the camera # really points to the target. self.position = position self.look_at(look_at) else: self.orientation = orientation # Set the position after the rotation so that it isn't affected. self.position = position @property def position(self): """ Get/set the position :type: :py:class:`mi.Point3f` """ return Camera.world_to_position(self._to_world) @position.setter def position(self, v): v = mi.Point3f(v) # Update transform c_to_world = self._to_world.matrix to_world = mi.Matrix4f( c_to_world[0].x, c_to_world[0].y, c_to_world[0].z, v.x, c_to_world[1].x, c_to_world[1].y, c_to_world[1].z, v.y, c_to_world[2].x, c_to_world[2].y, c_to_world[2].z, v.z, 0., 0., 0., 1.) self._to_world = mi.Transform4f(to_world) @property def orientation(self): r""" Get/set the orientation :type: :py:class:`mi.Point3f` """ return Camera.world_to_angles(self._to_world) @orientation.setter def orientation(self, v): v = mi.Point3f(v) # Mitsuba transform # Note: Mitsuba uses degrees v = v * 180.0 / dr.pi rot_x = mi.Transform4f().rotate(mi.Point3f(1, 0, 0), v.z) rot_y = mi.Transform4f().rotate(mi.Point3f(0, 1, 0), v.y) rot_z = mi.Transform4f().rotate(mi.Point3f(0, 0, 1), v.x) rot_mat = rot_z @ rot_y @ rot_x @ Camera.mi_to_sionna # Translation to keep the current position trs = mi.Transform4f().translate(self.position) to_world = trs @ rot_mat # Update in Mitsuba self._to_world = to_world
[docs] def look_at( self, target: rt.RadioDevice | rt.SceneObject | mi.Point3f) -> None: r""" Sets the orientation so that the camera looks at a position, scene object, or radio device Given a point :math:`\mathbf{x}\in\mathbb{R}^3` with spherical angles :math:`\theta` and :math:`\varphi`, the orientation of the camera will be set equal to :math:`(\varphi, \frac{\pi}{2}-\theta, 0.0)`. :param target: A position or object to look at. """ # Get position to look at if isinstance(target, (rt.RadioDevice, rt.SceneObject)): target = target.position else: target = mi.Point3f(target) # If the position and the target are on a line that is parallel to z, # then the look-at transform is ill-defined as z is up. # In this case, we add a small epsilon to x to avoid this. aligned = rt.isclose(self.position.x, target.x)\ & rt.isclose(self.position.y, target.y) aligned = aligned.numpy()[0] if aligned: target.x = target.x + 1e-3 # Look-at transform (recall Sionna uses Z-up) self._to_world = mi.Transform4f().look_at(self.position, target, mi.Vector3f(0.0, 0.0, 1.0))
############################################## # Internal methods and class functions. # Should not be appear in the end user # documentation. ############################################## @property def world_transform(self): r"""World transform, i.e., transform from local camera frame to world frame :type: :py:class:`mi.Transform4f` """ return self._to_world @staticmethod def world_to_angles(to_world: mi.Transform4f) -> mi.Point3f: r""" Extracts the orientation angles `[alpha,beta,gamma]` corresponding to a ``to_world`` transform :param to_world: Transform """ # Undo the rotation to switch from Mitsuba to Sionna convention to_world = to_world @ Camera.mi_to_sionna.inverse() r_mat = to_world.matrix # Compute angles x_ang = dr.atan2(r_mat[2,1], r_mat[2,2]) y_ang = dr.atan2(-r_mat[2,0], dr.sqrt(dr.square(r_mat[2,1]) + dr.square(r_mat[2,2]))) z_ang = dr.atan2(r_mat[1,0], r_mat[0,0]) return mi.Point3f(z_ang, y_ang, x_ang) @staticmethod def world_to_position(to_world: mi.Transform4f) -> mi.Point3f: r""" Extracts the translation component of a ``to_world`` transform. If it has multiple entries, throw an exception. :param to_world: Transform """ p = mi.Point3f(to_world.translation()) return p