Source code for sionna.rt.radio_materials.radio_material

#
# SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
"""Class implementing a radio material"""

import drjit as dr
import mitsuba as mi
from typing import Tuple, Callable

from sionna.rt.utils import itu_coefficients_single_layer_slab,\
    complex_relative_permittivity, jones_matrix_to_world_implicit
from sionna.rt.constants import InteractionType, DEFAULT_THICKNESS,\
    DEFAULT_FREQUENCY
from .radio_material_base import RadioMaterialBase
from .scattering_pattern import scattering_pattern_registry, \
                                ScatteringPattern

from scipy.constants import speed_of_light

[docs] class RadioMaterial(RadioMaterialBase): # pylint: disable=line-too-long r""" Class implementing the radio material model described in the `Primer on Electromagnetics <../em_primer.html>`_ This class implements :class:`RadioMaterialBase`. A radio material is defined by its relative permittivity :math:`\varepsilon_r` and conductivity :math:`\sigma` (see :eq:`eta`), its thickness :math:`d` (see :eq:`q_fresnel_slab`), as well as optional parameters related to diffuse reflection (see `Scattering`_), such as the scattering coefficient :math:`S`, cross-polarization discrimination coefficient :math:`K_x`, and scattering pattern :math:`f_\text{s}(\hat{\mathbf{k}}_\text{i}, \hat{\mathbf{k}}_\text{s})`. We assume non-ionized and non-magnetic materials, and therefore the permeability :math:`\mu` of the material is assumed to be equal to the permeability of vacuum i.e., :math:`\mu_r=1.0`. Reflection and refraction coefficients are computed assuming that the intersected surface models a slab with the specified ``thickness`` (see :eq:`fresnel_slab`). The computed coefficients therefore account for the multiple internal reflections inside the slab, meaning that this class should be used when objects like walls are modeled as single flat surfaces. The figure below illustrates this model, where :math:`E_i` is the incident electric field, :math:`E_r` is the reflected field and :math:`E_t` is the transmitted field. The Jones matrices, :math:`\mathbf{R}(d)` and :math:`\mathbf{T}(d)`, represent the effects of reflection and transmission, respectively, and depend on the slab thickness, :math:`d`. .. figure:: ../figures/transmission_model.png :width: 80% :align: center For frequency-dependent materials, it is possible to specify a callback function ``frequency_update_callback`` that computes the material properties :math:`(\varepsilon_r, \sigma)` from the frequency. If a callback function is specified, the material properties cannot be set and the values specified at instantiation are ignored. In addition to the following inputs, additional keyword arguments can be provided that will be passed to the scattering pattern as keyword arguments. :param name: Unique name of the material. Ignored if ``props`` is provided. :param thickness: Thickness of the material [m]. Ignored if ``props`` is provided. :param relative_permittivity: Relative permittivity of the material. Must be larger or equal to 1. Ignored if ``frequency_update_callback`` or ``props`` is provided. :param conductivity: Conductivity of the material [S/m]. Must be non-negative. Ignored if ``frequency_update_callback`` or ``props`` is provided. :param scattering_coefficient: Scattering coefficient :math:`S\in[0,1]` as defined in :eq:`scattering_coefficient`. Ignored if ``props`` is provided. :param xpd_coefficient: Cross-polarization discrimination coefficient :math:`K_x\in[0,1]` as defined in :eq:`xpd`. Only relevant if ``scattering_coefficient`` is not equal to zero. Ignored if ``props`` is provided. :param scattering_pattern: Name of a registered scattering pattern for diffuse reflections :list-registry:`sionna.rt.radio_materials.scattering_pattern_registry`. Only relevant if ``scattering_coefficient`` is not equal to zero. Ignored if ``props`` is provided. :param frequency_update_callback: Callable used to update the material parameters when the frequency is set. This callable must take as input the frequency [Hz] and must return the material properties as a tuple: ``(relative_permittivity, conductivity)``. If set to :py:class:`None`, then material properties are constant and equal to the value set at instantiation or using the corresponding setters. :param color: RGB (red, green, blue) color for the radio material as displayed in the previewer and renderer. Each RGB component must have a value within the range :math:`[0,1]`. If set to :py:class:`None`, then a random color is used. :param props: Mitsuba container storing the material properties, and used when loading a scene to initialize the radio material. Keyword Arguments ----------------- ** : :py:class:`Any` Depending on the chosen scattering antenna pattern, other keyword arguments must be provided. See the :ref:`Developer Guide <dev_custom_scattering_patterns>` for more details. """ # pylint: disable=line-too-long def __init__( self, name : str | None = None, thickness : float | mi.Float = DEFAULT_THICKNESS, relative_permittivity : float | mi.Float = 1.0, conductivity : float | mi.Float = 0.0, scattering_coefficient : float | mi.Float = 0.0, xpd_coefficient : float | mi.Float = 0.0, scattering_pattern : str = "lambertian", frequency_update_callback : Callable[[mi.Float], Tuple[mi.Float, mi.Float]] | None = None, color : Tuple[float, float, float] | None = None, props : mi.Properties | None = None, **kwargs): if props is None: props = self._build_mi_props_from_params(name, thickness, relative_permittivity, conductivity, scattering_coefficient, xpd_coefficient, color, **kwargs) # Real part of the relative permittivity eta_r = 1.0 if props.has_property('relative_permittivity'): eta_r = props['relative_permittivity'] props.remove_property('relative_permittivity') self.relative_permittivity = eta_r # Conductivity [S/m] sigma = 0.0 if props.has_property('conductivity'): sigma = props['conductivity'] props.remove_property('conductivity') self.conductivity = sigma # Material thickness [m] if props.has_property('thickness'): d = props['thickness'] props.remove_property('thickness') else: d = DEFAULT_THICKNESS self.thickness = d # Scattering coefficient s = 0.0 if props.has_property('scattering_coefficient'): s = props['scattering_coefficient'] props.remove_property('scattering_coefficient') self.scattering_coefficient = s # XPD coefficient kx = 0.0 if props.has_property("xpd_coefficient"): kx = props["xpd_coefficient"] props.remove_property('xpd_coefficient') self.xpd_coefficient = kx super().__init__(props) # Gather the other properties as keyword arguments for the # scattering pattern scattering_pattern_attributes = {} for prop_name in props.property_names(): scattering_pattern_attributes[prop_name] = props[prop_name] # Set the scattering pattern if provided if scattering_pattern is None: scattering_pattern = "lambertian" factory = scattering_pattern_registry.get(scattering_pattern) self.scattering_pattern = factory(**scattering_pattern_attributes) # Set the frequency update callback self.frequency_update_callback = frequency_update_callback @RadioMaterialBase.scene.setter def scene(self, scene): # We need to overwrite this setter to make sure that the material # parameters are correctly updated if a frequency callback is defined RadioMaterialBase.scene.fset(self, scene) self.frequency_update() @property def relative_permittivity(self): r""" Get/set the relative permittivity :math:`\varepsilon_r` :eq:`eta` :type: :py:class:`mi.Float` """ return self._eta_r @relative_permittivity.setter def relative_permittivity(self, eta_r): if eta_r < 1.0: raise ValueError("Real part of the relative permittivity must be" " greater or equal to 1") self._eta_r = mi.Float(eta_r) @property def conductivity(self): r"""Get/set the conductivity :math:`\sigma` [S/m] :eq:`eta` :type: :py:class:`mi.Float` """ return self._sigma @conductivity.setter def conductivity(self, sigma): if sigma < 0.0: raise ValueError("The conductivity must be greater or equal to 0") self._sigma = mi.Float(sigma) @property def thickness(self): r"""Get/set the material thickness [m] :type: :py:class:`mi.Float` """ return self._d @thickness.setter def thickness(self, d): if d < 0.0: raise ValueError("The material thickness must be positive") self._d = mi.Float(d) @property def scattering_coefficient(self): r"""Get/set the scattering coefficient :math:`S\in[0,1]` :eq:`scattering_coefficient` :type: :py:class:`mi.Float` """ return self._s @scattering_coefficient.setter def scattering_coefficient(self, s): if s < 0.0 or s > 1.0: raise ValueError("Scattering coefficient must be in range (0,1)") self._s = mi.Float(s) @property def xpd_coefficient(self): r"""Get/set the cross-polarization discrimination coefficient :math:`K_x\in[0,1]` :eq:`xpd` :type: :py:class:`mi.Float` """ return self._kx @xpd_coefficient.setter def xpd_coefficient(self, kx): if kx < 0.0 or kx > 1.0: raise ValueError("XPD coefficient must be in the range (0,1)") self._kx = mi.Float(kx) self._build_xpd_jones_mat() @property def scattering_pattern(self): # pylint: disable=line-too-long r"""Get/set the scattering pattern :type: :class:`~sionna.rt.ScatteringPattern` """ return self._scattering_pattern @scattering_pattern.setter def scattering_pattern(self, sp): if not isinstance(sp, ScatteringPattern): raise ValueError("Not an instance of ScatteringPattern") self._scattering_pattern = sp @property def frequency_update_callback(self): # pylint: disable=line-too-long r""" Get/set the frequency update callback :type: :py:class:`Callable` [[:py:class:`mi.Float`], [:py:class:`mi.Float`, :py:class:`mi.Float`]] """ return self._frequency_update_callback @frequency_update_callback.setter def frequency_update_callback(self, value): self._frequency_update_callback = value self.frequency_update()
[docs] def frequency_update(self): r"""Updates the material parameters according to the set frequency""" if self._frequency_update_callback is None: return if self._scene is None: return parameters = self._frequency_update_callback(self._scene.frequency) relative_permittivity, conductivity = parameters self.relative_permittivity = relative_permittivity self.conductivity = conductivity
def sample( self, ctx : mi.BSDFContext, si : mi.SurfaceInteraction3f, sample1 : mi.Float, sample2 : mi.Point2f, active : bool | mi.Bool = True ) -> Tuple[mi.BSDFSample3f, mi.Spectrum]: # pylint: disable=line-too-long r""" Samples the radio material This function samples an interaction type (e.g., specular reflection, diffuse reflection or refraction) and direction of propagation for the scattered ray, and returns the corresponding radio material sample and Jones matrix. The returned radio material sample stores the sampled type of interaction and sampled direction of propagation of the scattered ray. The following assumptions are made on the inputs: - ``ctx.component`` is a binary mask that specifies the types of interaction enabled. Booleans can be obtained from this mask as follows: .. code-block:: python specular_reflection_enabled = (ctx.component & InteractionType.SPECULAR) > 0 diffuse_reflection_enabled = (ctx.component & InteractionType.DIFFUSE) > 0 refraction_enabled = (ctx.component & InteractionType.REFRACTION) > 0 - ``si.wi`` is the direction of propagation of the incident wave in the world frame - ``si.sh_frame`` is the frame such that the ``sh_frame.n`` is the normal to the intersected surface in the world coordinate system - ``si.dn_du`` stores the real part of the S and P components of the incident electric field represented in the implicit world frame (first and second components of ``si.dn_du``) - ``si.dn_dv`` stores the imaginary part of the S and P components of the incident electric field represented in the implicit world frame (first and second components of ``si.dn_dv``) - ``si.dp_du`` stores the solid angle of the ray tube (first component of ``si.dn_du``) The outputs are set as follows: - ``bs.wo`` is the direction of propagation of the sampled scattered ray in the world frame - ``jones_mat`` is the Jones matrix describing the transformation incurred to the incident wave in the implicit world frame :param ctx: A context data structure used to specify which interaction types are enabled :param si: Surface interaction data structure describing the underlying surface position :param sample1: A uniformly distributed sample on :math:`[0,1]` used to sample the type of interaction :param sample2: A uniformly distributed sample on :math:`[0,1]^2` used to sample the direction of the reflected wave in the case of diffuse reflection :param active: Mask to specify active rays :return: Radio material sample and Jones matrix as a :math:`4 \times 4` real-valued matrix """ # Incident direction of propagation in the local frame ki_local = si.wi # Cosine of the angle of arrival # In the local interaction frame, the normal is z+ cos_theta_i = -ki_local.z # Scattering types of interactions to sample spec_ref_enabled = (ctx.component & InteractionType.SPECULAR) > 0 diff_ref_enabled = (ctx.component & InteractionType.DIFFUSE) > 0 trans_enabled = (ctx.component & InteractionType.REFRACTION) > 0 # If scene is not set, uses the default frequency for evaluating the # material. # This is required to enable Dr.Jit to trace this code when the material # is instantiated but not assigned to a scene yet. if self._scene is None: angular_frequency = dr.two_pi*DEFAULT_FREQUENCY wavelength = speed_of_light/DEFAULT_FREQUENCY else: angular_frequency = self._scene.angular_frequency wavelength = self._scene.wavelength # To-world transform as a 3x3 matrix to_world = mi.Matrix3f(si.sh_frame.s, si.sh_frame.t, si.sh_frame.n).T # Relative permittivity of the material eta = complex_relative_permittivity(self._eta_r, self._sigma, angular_frequency) # ITU coefficients # r_te, r_tm: TE/TM reflection coefficients # t_te, t_tm: TE/TM transmission coefficients r_te, r_tm, t_te, t_tm =\ itu_coefficients_single_layer_slab(cos_theta_i, eta, self._d, wavelength) # Sample an event type: Specular reflection or refraction sampled_event, reflection, diffuse, probs\ = self._sample_event_type(sample1, r_te, r_tm, t_te, t_tm, spec_ref_enabled, diff_ref_enabled, trans_enabled) # Direction of propagation of specularly reflected and transmitted # wave ko_spec_trans_local =\ self._specular_reflection_transmission_direction(ki_local, reflection) # Direction of propagation of scattered wave for the diffuse reflection ko_diff_local = self._diffuse_reflection_direction(sample2) # Computes the Jones matrix and direction of propagation of the # scattered wave for specular reflection and transmission. # Because the specular reflection matrix is needed to compute the # diffusely scattered field, it is also computed when a diffuse # reflection was sampled. spec_trans_mat = self._specular_reflection_transmission_matrix(to_world, ki_local, ko_spec_trans_local, reflection, r_te, r_tm, t_te, t_tm) # Computes Jones matrix for diffuse reflection diff_mat = self._diffuse_reflection_matrix(si, ki_local, ko_diff_local, spec_trans_mat) # Jones matrix selected according to the sampled event type jones_mat = dr.select(diffuse, diff_mat, spec_trans_mat) # Apply multiplication by scattering coefficient and the importance # sampling weighting. # Scaling by `1/sqrt(probs)` cancels the weighting by `probs` that # arises from sampling the interaction types according to these # probabilities. s = dr.select(reflection, dr.sqrt(1. - dr.square(self._s)), 1.) s = dr.select(diffuse, self._s, s) # Block differentiation through the importance sampling weighting jones_mat *= s*dr.detach(dr.rsqrt(probs)) # Direction of propagation of the scattered wave selected according to # the sampled event type ko_local = dr.select(diffuse, ko_diff_local, ko_spec_trans_local) # Cast the Jones matrix to a mi.Spectrum to meet the requirements of # the BSDF interface jones_mat = mi.Spectrum(jones_mat) # Instantiate and set the BSDFSample object bs = mi.BSDFSample3f() bs.sampled_component = sampled_event # Direction of the scattered wave in the world frame bs.wo = to_world@ko_local # Not used but must be set bs.sampled_type = mi.UInt32(+mi.BSDFFlags.DeltaReflection) bs.pdf = probs bs.eta = 1.0 return bs, jones_mat def eval( self, ctx : mi.BSDFContext, si : mi.SurfaceInteraction3f, wo : mi.Vector3f, active : bool | mi.Bool = True ) -> mi.Spectrum: # pylint: disable=line-too-long r""" Evaluates the radio material This function evaluates the Jones matrix of the radio material for the scattered direction ``wo`` and for the interaction type stored in ``si.prim_index``. The following assumptions are made on the inputs: - ``si.wi`` is the direction of propagation of the incident wave in the world frame - ``si.sh_frame`` is the frame such that the ``sh_frame.n`` is the normal to the intersected surface in the world coordinate system - ``si.dn_du`` stores the real part of the S and P components of the incident electric field represented in the implicit world frame (first and second components of ``si.dn_du``) - ``si.dn_dv`` stores the imaginary part of the S and P components of the incident electric field represented in the implicit world frame (first and second components of ``si.dn_dv``) - ``si.dp_du`` stores the solid angle of the ray tube (first component of ``si.dn_du``) - ``si.prim_index`` stores the interaction type to evaluate :param ctx: A context data structure used to specify which interaction types are enabled :param si: Surface interaction data structure describing the underlying surface position :param wo: Direction of propagation of the scattered wave in the world frame :param active: Mask to specify active rays :return: Jones matrix as a :math:`4 \times 4` real-valued matrix """ # Incident direction of propagation in the local frame ki_local = si.wi # Cosine of the angle of arrival # In the local interaction frame, the normal is z+ cos_theta_i = -ki_local.z # If scene is not set, uses the default frequency for evaluating the # material. # This is required to enable Dr.Jit to trace this code when the material # is instantiated but not assigned to a scene yet. if self._scene is None: angular_frequency = dr.two_pi*DEFAULT_FREQUENCY wavelength = speed_of_light/DEFAULT_FREQUENCY else: angular_frequency = self._scene.angular_frequency wavelength = self._scene.wavelength # Direction of propagation of the scattered wave in the world frame ko_world = wo ko_local = si.to_local(ko_world) # To-world transform as a 3x3 matrix to_world = mi.Matrix3f(si.sh_frame.s, si.sh_frame.t, si.sh_frame.n).T # Relative permittivity of the material eta = complex_relative_permittivity(self._eta_r, self._sigma, angular_frequency) # ITU coefficients # r_te, r_tm: TE/TM reflection coefficients # t_te, t_tm: TE/TM transmission coefficients r_te, r_tm, t_te, t_tm =\ itu_coefficients_single_layer_slab(cos_theta_i, eta, self._d, wavelength) sampled_event = si.prim_index reflection = (sampled_event == InteractionType.SPECULAR) |\ (sampled_event == InteractionType.DIFFUSE) diffuse = sampled_event == InteractionType.DIFFUSE # Direction of propagation of specularly reflected and transmitted # wave ko_spec_trans_local =\ self._specular_reflection_transmission_direction(ki_local, reflection) # Computes the Jones matrix and direction of propagation of the # scattered wave for specular reflection and transmission. # Because the specular reflection matrix is needed to compute the # diffusely scattered field, it is also computed when a diffuse # reflection was sampled. spec_trans_mat = self._specular_reflection_transmission_matrix(to_world, ki_local, ko_spec_trans_local, reflection, r_te, r_tm, t_te, t_tm) # Computes Jones matrix for diffuse reflection diff_mat = self._diffuse_reflection_matrix(si, ki_local, ko_local, spec_trans_mat) # Jones matrix selected according to the sampled event type jones_mat = dr.select(diffuse, diff_mat, spec_trans_mat) # Apply multiplication by scattering coefficient s = dr.select(reflection, dr.sqrt(1. - dr.square(self._s)), 1.) s = dr.select(diffuse, self._s, s) jones_mat *= s # Cast the Jones matrix to a mi.Spectrum to meet the requirements of # the BSDF interface jones_mat = mi.Spectrum(jones_mat) return jones_mat def pdf( self, ctx : mi.BSDFContext, si : mi.SurfaceInteraction3f, wo : mi.Vector3f, active : bool | mi.Bool = True ) -> mi.Float: # pylint: disable=line-too-long r""" Evaluates the probability of the sampled interaction type and direction of scattered ray This function evaluates the probability density of the radio material for the scattered direction ``wo`` and for the interaction type stored in ``si.prim_index``. The following assumptions are made on the inputs: - ``si.wi`` is the direction of propagation of the incident wave in the world frame - ``si.sh_frame`` is the frame such that the ``sh_frame.n`` is the normal to the intersected surface in the world coordinate system - ``si.dn_du`` stores the real part of the S and P components of the incident electric field represented in the implicit world frame (first and second components of ``si.dn_du``) - ``si.dn_dv`` stores the imaginary part of the S and P components of the incident electric field represented in the implicit world frame (first and second components of ``si.dn_dv``) - ``si.dp_du`` stores the solid angle of the ray tube (first component of ``si.dn_du``) - ``si.prim_index`` stores the interaction type to evaluate :param ctx: A context data structure used to specify which interaction types are enabled :param si: Surface interaction data structure describing the underlying surface position :param wo: Direction of propagation of the scattered wave in the world frame :param active: Mask to specify active rays :return: Probability density value """ # Incident direction of propagation in the local frame ki_local = si.wi # Cosine of the angle of arrival # In the local interaction frame, the normal is z+ cos_theta_i = -ki_local.z # If scene is not set, uses the default frequency for evaluating the # material. # This is required to enable Dr.Jit to trace this code when the material # is instantiated but not assigned to a scene yet. if self._scene is None: angular_frequency = dr.two_pi*DEFAULT_FREQUENCY wavelength = speed_of_light/DEFAULT_FREQUENCY else: angular_frequency = self._scene.angular_frequency wavelength = self._scene.wavelength # Scattering types of interactions to sample spec_ref_enabled = (ctx.component & InteractionType.SPECULAR) > 0 diff_ref_enabled = (ctx.component & InteractionType.DIFFUSE) > 0 trans_enabled = (ctx.component & InteractionType.REFRACTION) > 0 # Relative permittivity of the material eta = complex_relative_permittivity(self._eta_r, self._sigma, angular_frequency) # ITU coefficients # r_te, r_tm: TE/TM reflection coefficients # t_te, t_tm: TE/TM transmission coefficients r_te, r_tm, t_te, t_tm =\ itu_coefficients_single_layer_slab(cos_theta_i, eta, self._d, wavelength) sampled_event = si.prim_index reflection = (sampled_event == InteractionType.SPECULAR) |\ (sampled_event == InteractionType.DIFFUSE) diffuse = sampled_event == InteractionType.DIFFUSE prs, prd, pt, _ = self._event_probabilities(r_te, r_tm, t_te, t_tm, spec_ref_enabled, diff_ref_enabled, trans_enabled) probs = dr.select(diffuse, prd, prs) probs = dr.select(reflection, probs, pt) return probs def traverse(self, callback : mi.TraversalCallback): # pylint: disable=line-too-long r""" Traverse the attributes and objects of this instance Implementing this function is required for Mitsuba to traverse a scene graph, including materials, and determine the differentiable parameters. :param callback: Object used to traverse the scene graph """ callback.put_parameter('eta_r', self._eta_r, mi.ParamFlags.Differentiable) callback.put_parameter('sigma', self._sigma, mi.ParamFlags.Differentiable) callback.put_parameter('d', self._d, mi.ParamFlags.Differentiable) callback.put_parameter('s', self._s, mi.ParamFlags.Differentiable) callback.put_parameter('xpd_coefficient', self._kx, mi.ParamFlags.Differentiable) def to_string(self) -> str: r""" Returns a string describing the object """ s = f"RadioMaterial eta_r={self._eta_r[0]:.3f}\n"\ f" sigma={self._sigma[0]:.3f}\n"\ f" thickness={self._d[0]:.3f}\n"\ f" scattering_coefficient={self._s[0]:.3f}\n"\ f" xpd_coefficient={self._kx[0]:.3f}" return s ############################################## # Internal methods ############################################## def _event_probabilities( self, r_te : mi.Complex2f, r_tm : mi.Complex2f, t_te : mi.Complex2f, t_tm : mi.Complex2f, spec_ref_enabled : bool, diff_ref_enabled: bool, trans_enabled: bool ) -> Tuple[mi.Float, mi.Float, mi.Float]: # pylint: disable=line-too-long r""" Compute probabilities with which to sample interaction types The probabilities of sampling an interaction type is set proportionally to the corresponding gain. To compute the probability of sampling a transmission or a reflection, it is assumed that the energy is evenly split between the two polarization directions. :param r_te: Transverse electric reflection coefficient :param r_tm: Transverse magnetic reflection coefficient :param t_te: Transverse electric refraction coefficient :param t_tm: Transverse magnetic refraction coefficient :param spec_ref_enabled: Flag indicating if specular reflection is enabled :param diff_ref_enabled: Flag indicating if diffuse reflection is enabled :param trans_enabled: Flag indicating if refraction is enabled :return: Probability with which to sample a specular reflection :return: Probability with which to sample a diffuse reflection :return: Probability with which to sample a transmission :return: Flag indicating if the sampled interaction type is NONE """ # The probability of reflection and transmission are sampled # proportionally to the reflection and transmission coefficients r = dr.square(dr.abs(r_te)) + dr.square(dr.abs(r_tm)) t = dr.square(dr.abs(t_te)) + dr.square(dr.abs(t_tm)) # Scattering coefficient s = dr.square(self._s) # Probability of sampling a specular reflection prs = r*(1.-s) if spec_ref_enabled else mi.Float(0.) # Probability of sampling a diffuse reflection prd = r*s if diff_ref_enabled else mi.Float(0.) # Probability of a transmission pt = t if trans_enabled else mi.Float(0.) # Normalize to ensure the probabilities sum to 1 sum_probs = prs + prd + pt none_int = sum_probs <= 0. norm_factor = dr.select(none_int, 1.0, dr.rcp(sum_probs)) prs *= norm_factor prd *= norm_factor pt *= norm_factor return prs, prd, pt, none_int def _sample_event_type( self, sample1 : mi.Float, r_te : mi.Complex2f, r_tm : mi.Complex2f, t_te : mi.Complex2f, t_tm : mi.Complex2f, spec_ref_enabled : bool, diff_ref_enabled: bool, trans_enabled: bool ) -> Tuple[mi.UInt, mi.Bool, mi.Bool, mi.Float]: # pylint: disable=line-too-long """ Samples event types :param sample1: Uniformly distributed float in :math:`[0,1]` :param r_te: Transverse electric reflection coefficient :param r_tm: Transverse magnetic reflection coefficient :param t_te: Transverse electric refraction coefficient :param t_tm: Transverse magnetic refraction coefficient :param spec_ref_enabled: Flag indicating if specular reflection is enabled :param diff_ref_enabled: Flag indicating if diffuse reflection is enabled :param trans_enabled: Flag indicating if refraction is enabled :return: Sampled type of interaction :return: Flag set to `True` if a reflection (specular or diffuse) was sampled :return: Flag set to `True` if a diffuse reflection was sampled :return: Probability of sampling the selected event """ # If all scattering event are disabled, then just return a NONE # interaction if not (spec_ref_enabled or diff_ref_enabled or trans_enabled): return InteractionType.NONE, mi.Bool(False), mi.Bool(False),\ mi.Float(0.) prs, prd, pt, none_int = self._event_probabilities(r_te, r_tm, t_te, t_tm, spec_ref_enabled, diff_ref_enabled, trans_enabled) reflection = sample1 < prs + prd diffuse = sample1 < prd reflection_type = dr.select(diffuse, InteractionType.DIFFUSE, InteractionType.SPECULAR) sampled_event = dr.select(reflection, reflection_type, InteractionType.REFRACTION) sampled_event = dr.select(none_int, InteractionType.NONE, sampled_event) probs = dr.select(diffuse, prd, prs) probs = dr.select(reflection, probs, pt) return sampled_event, reflection, diffuse, probs def _specular_reflection_transmission_direction( self, ki_local : mi.Vector3f, reflection : mi.Bool ) -> mi.Vector3f: # pylint: disable=line-too-long r""" Computes the direction of propagation of specularly reflected and transmitted wave according to the sampled event, indicated by the ``reflection`` mask :param ki_local: Direction of propagation of the incident field in the local frame :param reflection: Mask indicating if a reflection (specular or diffuse) was sampled :return: Direction of propagation of the scattered wave in the local frame """ # Direction of the scattered field ko_local_spec_refl = mi.reflect(-ki_local) ko_local_trans = ki_local ko_local = dr.select(reflection, ko_local_spec_refl, ko_local_trans) return ko_local def _specular_reflection_transmission_matrix( self, to_world : mi.Matrix3f, ki_local : mi.Vector3f, ko_local : mi.Vector3f, reflection : mi.Bool, r_te : mi.Complex2f, r_tm : mi.Complex2f, t_te : mi.Complex2f, t_tm : mi.Complex2f ) -> mi.Matrix4f: # pylint: disable=line-too-long r""" Computes the Jones matrix of the scattered wave for specular reflection and transmission according to the sampled event indicated by the ``reflection`` mask :param to_world: To-world transform as a :math:`3 \times 3` matrix :param ki_local: Direction of propagation of the incident field in the local frame :param ko_local: Direction of propagation of the scattered wave in the local frame :param reflection: Mask indicating if a reflection (specular or diffuse) was sampled :param r_te: Transverse electric reflection coefficient :param r_tm: Transverse magnetic reflection coefficient :param t_te: Transverse electric refraction coefficient :param t_tm: Transverse magnetic refraction coefficient :return: Jones matrix as a :math:`4 \times 4` real-valued matrix in the world frame """ # Selects the reflection/refraction coefficients, direction of # scattering, and S basis vector according to the sampled event. # Coefficients c1 = dr.select(reflection, r_te, t_te) c2 = dr.select(reflection, r_tm, t_tm) # Jones matrix in the world implicit base jones_mat = jones_matrix_to_world_implicit(c1, c2, to_world, ki_local, ko_local) return jones_mat def _diffuse_reflection_direction( self, sample2 : mi.Point2f ) -> mi.Vector3f: # pylint: disable=line-too-long r""" Computes a unit vector on the hemisphere from a 2D uniform sample on the unit square `sample2` :param sample2: A uniformly distributed sample on :math:`[0,1]^2` :return: Unit vector on the hemisphere """ ko_local = mi.warp.square_to_uniform_hemisphere(sample2) # Due to numerical error, it could be that kd_local.z is slightly # negative ko_local.z = dr.abs(ko_local.z) return ko_local def _diffuse_reflection_matrix( self, si : mi.SurfaceInteraction3f, ki_local : mi.Vector3f, ko_local : mi.Vector3f, specular_reflection_mat : mi.Matrix4f ) -> mi.Matrix4f: # pylint: disable=line-too-long r""" Computes the Jones matrix for diffuse reflection :param si: Surface interaction data structure describing the underlying surface position :param ki_local: Direction of propagation of the incident wave in the local frame :param ko_local: Direction of propagation of the scattered wave in the local frame :param specular_reflection_mat: Jones matrix for specular reflection as :math:`4 \times 4` real-valued matrix in the implicit world frame :return: Jones matrix as a :math:`4 \times 4` real-valued matrix in the world frame """ # `si.dn_du` is used to store the real components of the S and P # coefficients of the incident field # `si.dn_dv` is used to store the imaginary components of the S and P # coefficients of the incident field # `si.dp_du` is used to store the solid angle # Note that the incident field is represented in the implicit world # frame # Incident field Jones vector ei = mi.Vector4f(si.dn_du.x, # Real component of S si.dn_du.y, # Real component of P si.dn_dv.x, # Imag component of S si.dn_dv.y) # Imag component of P # Solid angle solid_angle = si.dp_du.x # Amplitude of the reflected field er_spec = specular_reflection_mat@ei er_spec_norm = dr.norm(er_spec) # Amplitude of the incident field ei_norm = dr.norm(ei) # Gamma coefficient gamma = dr.select(ei_norm > 0., er_spec_norm*dr.rcp(ei_norm), mi.Float(0.)) # Scattering pattern fs = self._scattering_pattern(ki_local, ko_local) # Jones matrix for diffuse reflection # As the implicit basis are the spherical unit vectors # (theta_hat, phi_hat), we do not need to apply change-of-basis matrices # from the implicit basis to the spherical basis. Note that this would # be needed otherwise, as the model used for diffuse scattering operates # in the basis defined by the spherical unit vectors. jones_mat = dr.sqrt(fs*solid_angle)*gamma*self._xpd_jones_mat return jones_mat def _build_xpd_jones_mat(self): # pylint: disable=line-too-long r""" Builds the Jones matrix from the XPD coefficient that models the rotation of the polarization direction The stored Jones matrix is represented by a :math:`4 \times 4` real-valued matrix as follows: .. math:: J = \begin{bmatrix} \begin{array}{c|c} \sqrt(1-K_x) & -\sqrt(Kx) & 0 & 0 \\ \hline \sqrt(Kx) & \sqrt(1-K_x) & 0 & 0 \\ \hline 0 & 0 & \sqrt(1-K_x) & -\sqrt(Kx) \\ \hline 0 & 0 & \sqrt(K_x) & \sqrt(1-Kx) \\ \end{array} \end{bmatrix} where :math:`K_x` is the XPD coefficient. The built Jones matrix is stored in the `self._xpd_jones_mat` attribute. """ a = dr.sqrt(1. - self._kx) b = dr.sqrt(self._kx) m = mi.Matrix4f(a, -b, 0., 0., b, a, 0., 0., 0., 0., a, -b, 0., 0., b, a) self._xpd_jones_mat = m def _build_mi_props_from_params( self, name : str, thickness : float | mi.Float, relative_permittivity : float | mi.Float, conductivity : float | mi.Float, scattering_coefficient : float | mi.Float, xpd_coefficient : float | mi.Float, color : Tuple[float, float, float] | None, **kwargs ) -> mi.Properties: # pylint: disable=line-too-long r""" Builds a :class:`mitsuba.Properties` object from a set of material properties Additional keyword arguments can be provided to be passed to the scattering pattern. :param name: Unique name of the material :param thickness: Thickness of the material [m] :param relative_permittivity: Relative permittivity of the material :param conductivity: Conductivity of the material [S/m] :param scattering_coefficient: Scattering coefficient :param xpd_coefficient: Cross-polarization discrimination coefficient :param color: Optional RGB (red, green, blue) color for the radio material as displayed in the previewer and renderer """ props = mi.Properties("radio-material") # Name of the radio material props.set_id(name) # BSDF parameters props["relative_permittivity"] = relative_permittivity props["conductivity"] = conductivity props["scattering_coefficient"] = scattering_coefficient props["thickness"] = thickness props["xpd_coefficient"] = xpd_coefficient if color is not None: props["color"] = mi.ScalarColor3f(color) # Additional keywords arguments will be passed to the # scattering pattern for k, v in kwargs.items(): props[k] = v return props
# Register this custom BSDF plugin mi.register_bsdf("radio-material", lambda props: RadioMaterial(props=props))