"""
Lights can be used to illuminate your models. They have a position and an orientation which can be set via
the standard orientation attributes of :class:`blueprints.thing.moveable.MoveableThing`. Mujoco uses the
:attr:`Light.dir` attribute internally, so blueprint synchronizes all changes in ``euler``, ``alpha``, ``beta``,
``gamma`` with ``dir`` internally.
Similarly to :class:`blueprints.camera.Camera` Lights can either be fixed in orientation locally/globally or
tracking another Body by setting the :attr:`Light.mode` and optionally :attr:`Light.target`. For a detailed
description see :attr:`Light.mode`.
.. code-block::
:caption: Local orientation
>>> body = blue.Body(lights=blue.Light(mode='fixed'))
.. code-block::
:caption: Global orientation
>>> body = blue.Body(lights=blue.Light(mode='track'))
.. code-block::
:caption: Tracking Body
>>> one = blue.Body(name='one', lights=blue.Light(mode='targetbody'))
>>> two = blue.Body(name='two')
>>> root = blue.Body(name='root', bodies=[one, two])
>>> # Version one:
>>> root.bodies['one'].lights.target = root.bodies['two']
>>> # Version two:
>>> root.bodies['two'].targeting_lights = root.bodies['one'].lights
.. code-block:: mxml
:caption: XML example
<body name="root">
<body name="one">
<light mode="targetbody" name="unnamed_light" cutoff="90.0" target="two" />
</body>
<body name="two" />
</body>
"""
import numpy as np
import xml.etree.ElementTree as xml
import blueprints as blue
from blueprints.utils.geometry import TAU, PI
[docs]
class Light(blue.LightType, blue.thing.CyclicalThing, blue.MoveableThing):
"""
This class is available through the shortcut :class:`blueprints.Light <Light>`.
Most attribute descriptions are partially taken from `Mujoco <https://mujoco.readthedocs.io/en/latest/XMLreference.html#body-light>`__.
"""
[docs]
@blue.restrict
def __init__(self,
pos: np.ndarray|list[int|float] = [0., 0., 0.],
dir: np.ndarray|list[int|float] = [0., 0.,-1.],
attenuation: np.ndarray|list[int|float] = [1., 0., 0.],
ambient: np.ndarray|list[int|float] = [0., 0., 0.],
diffuse: np.ndarray|list[int|float] = [.7, .7, .7],
specular: np.ndarray|list[int|float] = [.3, .3, .3],
mode: str = 'fixed',
directional: bool = False,
castshadow: bool = True,
active: bool = True,
cutoff: float|int = TAU,
exponent: float|int = 10.,
name: str|None = None,
x: int|float|np.int32|np.int64|np.float32|np.float64|None = None,
y: int|float|np.int32|np.int64|np.float32|np.float64|None = None,
z: int|float|np.int32|np.int64|np.float32|np.float64|None = None,
**kwargs):
"""
Parameters
----------
pos : np.ndarray | list[int | float], optional
Represents the position of the Light. Changing this attribute also changes the properties
:attr:`x`, :attr:`y` and :attr:`z`.
dir : np.ndarray | list[int | float], optional
Direction of the Light.
attenuation : np.ndarray | list[int | float], optional
These are the constant, linear and quadratic attenuation coefficients in OpenGL. The default
corresponds to no attenuation. See the OpenGL documentation for more information on this and
all other OpenGL-related properties.
ambient : np.ndarray | list[int | float], optional
The ambient color of the Light.
diffuse : np.ndarray | list[int | float], optional
The diffuse color of the Light.
specular : np.ndarray | list[int | float], optional
The specular color of the Light.
mode : str, optional
The mode defines who the position and orientation is changed over time. See :attr:`mode`
for a detailed description.
directional : bool, optional
The light is directional if this attribute is True, otherwise it is a spotlight.
castshadow : bool, optional
If this attribute is True the Light will cast shadows. More precisely, the geoms
illuminated by the light will cast shadows, however this is a property of Lights rather
than geoms. Since each shadow-casting light causes one extra rendering pass through all
geoms, this attribute should be used with caution.
active : bool, optional
The Light is active if this attribute is True. This can be used at runtime to turn Lights
on and off.
cutoff : float, optional
Cutoff angle for spotlights in radians.
exponent : float, optional
Exponent for spotlights. This setting controls the softness of the spotlight cutoff.
name : str | None, optional
The user specified name of the Camera. It might change in the case of a naming conflict.
x : int | float |np.int32 | np.int64 | np.float32 | np.float64 | None, optional
If `pos` is not specified, this argument sets the X position coordinate.
y : int | float |np.int32 | np.int64 | np.float32 | np.float64 | None, optional
If `pos` is not specified, this argument sets the Y position coordinate.
z : int | float |np.int32 | np.int64 | np.float32 | np.float64 | None, optional
If `pos` is not specified, this argument sets the Z position coordinate.
**kwargs
Keyword arguments are passed to ``super().__init__``.
"""
assert mode in self._MODES
if 'euler' in kwargs:
alpha, beta, gamma = kwargs['euler']
R = blue.geometry.Rotation.E_rot(alpha, beta, gamma)
dir = R.T @ dir
# ASSIGN ATTRIBUTES
self.dir = dir
self.attenuation = attenuation
self.ambient = ambient
self.diffuse = diffuse
self.specular = specular
self.mode = mode
self.directional = directional
self.castshadow = castshadow
self.active = active
self.cutoff = cutoff
self.exponent = exponent
super().__init__(pos=pos,
name=name,
x=x,
y=y,
z=z,
**kwargs)
@blue.restrict
def _build(self, parent, world, indicies, **kwargs):
"""
This method is called to build the xml.
Parameters
----------
parent : xml.etree.ElementTree.Element
The xml element of its parent
world : WorldType
The World from which the build method was called initially
Returns
-------
xml.etree.ElementTree.Element
The builded xml element of the Light.
"""
self._index = indicies['light']
indicies['light'] += 1
cutoff = self.cutoff * blue.geometry.RADIANS_TO_DEGREES
if self.target is not None:
return super()._build(parent, world, indicies, cutoff=cutoff, target=self.target.name)
else:
return super()._build(parent, world, indicies, cutoff=cutoff)
@blue.restrict
@classmethod
def _from_xml_element(cls, xml_element: xml.Element) -> blue.ThingType:
"""
This method reconstructs a Light from an xml element.
Parameters
----------
xml_element : xml.Element
The xml element from which a Light is reconstructed.
Returns
-------
blue.ThingType
The reconstructed Light.
"""
init_args, post_args, rest_args = cls._xml_element_args(xml_element)
init_args['copy'] = False
if 'cutoff' in init_args:
init_args['cutoff'] = blue.geometry.DEGREES_TO_RADIANS * init_args['cutoff']
obj = object.__new__(cls)
obj.__init__(**init_args)
for key, val in post_args.items():
setattr(obj, key, val)
if 'target' in rest_args:
obj._target_name = rest_args['target']
return obj
# KINEMATIC TREE PROPERTIES
@property
def target(self) -> blue.BodyType|None:
"""
The target attribute specifies a Body that is tracked by the camera. Compatible :attr:`mode`
values are ('targetbody', 'targetbodycom').
Returns
-------
blue.BodyType | None
"""
if hasattr(self, '_target'):
return self._target
else:
return None
@target.setter
@blue.restrict
def target(self, target: blue.BodyType|None) -> None:
"""
The target attribute specifies a Body that is tracked by the Light. Compatible :attr:`mode`
values are ('targetbody', 'targetbodycom').
Parameters
----------
target : blue.BodyType | None
A body which is tracked by the Light. The Body must be part of the same kinematic tree
as the Light.
Raises
------
ValueError
If the parent is not part of the same kinematic tree or the Light has no parent an error
is raised.
"""
if self.parent is None and not self._IGNORE_CHECKS:
raise ValueError(f'To set the target reference, parent must be set first.')
if self.parent is not None and self.parent is target:
raise ValueError(f'Light target cannot be the same as its parent. Use the modes ("fixed", "track" "trackcom") instead.')
if self.target is not None:
self.target.detach(self)
if target is not None:
target._targeting_lights.append(self)
self._target = target
# MUJOCO PROPERTIES
@property
def mode(self) -> str:
"""
This argument works identically to :attr:`blueprints.camera.Camera.mode`. Possible values are:
* ``'fixed'``: Position and orientation will change w.r.t. the parents frame of reference.
* ``'track'``: Position is at a constant offset from the parent in world coordinates, while the
camera orientation is constant in world coordinates.
* ``'trackcom'``: is similar to ``'track'`` but the constant spatial offset is defined relative
to the center of mass of the kinematic subtree starting at the parent Body. This can be used to
keep an entire mechanism in view. Note that the subtree center of mass for the World is the
center of mass of the entire model.
* ``'targetbody'``: the Camera position is fixed in the parent Body, while the Camera orientation
is adjusted so that it always points towards the targeted Body (which is specified with the
target attribute)
* ``'targetbodycom'``: is the same as ``'targetbody'`` but the Camera is oriented towards the
center of mass of the subtree starting at the target Cody.
Returns
-------
str
When setting this value manually, it must be one of those specified above, otherwise an error is raised.
"""
return self._mode
@mode.setter
@blue.restrict
def mode(self, mode: str) -> None:
"""
This argument works identically to :attr:`blueprints.camera.Camera.mode`. Possible values are:
* ``'fixed'``: Position and orientation will change w.r.t. the parents frame of reference.
* ``'track'``: Position is at a constant offset from the parent in world coordinates, while the
camera orientation is constant in world coordinates.
* ``'trackcom'``: is similar to ``'track'`` but the constant spatial offset is defined relative
to the center of mass of the kinematic subtree starting at the parent Body. This can be used to
keep an entire mechanism in view. Note that the subtree center of mass for the World is the
center of mass of the entire model.
* ``'targetbody'``: the Camera position is fixed in the parent Body, while the Camera orientation
is adjusted so that it always points towards the targeted Body (which is specified with the
target attribute)
* ``'targetbodycom'``: is the same as ``'targetbody'`` but the Camera is oriented towards the
center of mass of the subtree starting at the target Cody.
Parameters
----------
mode : str
The attribute must be one of those specified above.
Raises
------
ValueError
If ``mode`` is not a valid value an error is raised.
"""
if mode not in self._MODES:
raise ValueError(f'The allowed values for mode are {self._MODES}, got {mode} instead.')
self._mode = mode
@property
def dir(self) -> np.ndarray:
"""
Direction of the light.
Returns
-------
np.ndarray
"""
return self.rotation_matrix @ self._dir.copy()
@dir.setter
@blue.restrict
def dir(self, dir: np.ndarray|list[int|float]) -> None:
"""
Parameters
----------
dir : np.ndarray | list[int | float]
Direction of the light.
Raises
------
Exception
If the shape of the dir argument is not (3,) and error is raised.
"""
if isinstance(dir, np.ndarray) and dir.shape != (3,):
raise Exception(f'Position attribute dir must have a dimension of 3 got {dir.shape} instead.')
if isinstance(dir, list) and len(dir) != 3:
raise Exception(f'Position attribute dir must have a dimension of 3 got {len(dir)} instead.')
self._dir = np.array(dir, dtype=np.float32)
@property
def attenuation(self) -> np.ndarray:
"""
These are the constant, linear and quadratic attenuation coefficients in OpenGL. The
default corresponds to no attenuation. See the OpenGL documentation for more information
on this and all other OpenGL-related properties.
Returns
-------
np.ndarray
"""
return self._attenuation.copy()
@attenuation.setter
@blue.restrict
def attenuation(self, attenuation: np.ndarray|list[int|float]) -> None:
"""
Parameters
----------
attenuation : np.ndarray | list[int | float]
These are the constant, linear and quadratic attenuation coefficients in OpenGL. The
default corresponds to no attenuation. See the OpenGL documentation for more information
on this and all other OpenGL-related properties.
Raises
------
Exception
If the shape of the dir argument is not (3,) and error is raised.
"""
if isinstance(attenuation, np.ndarray) and attenuation.shape != (3,):
raise Exception(f'Position attribute attenuation must have a dimension of 3 got {attenuation.shape} instead.')
if isinstance(attenuation, list) and len(attenuation) != 3:
raise Exception(f'Position attribute attenuation must have a dimension of 3 got {len(attenuation)} instead.')
self._attenuation = np.array(attenuation, dtype=np.float32)
@property
def ambient(self) -> np.ndarray:
"""
The ambient color of the light (RGB).
Returns
-------
np.ndarray
"""
return self._ambient.copy()
@ambient.setter
@blue.restrict
def ambient(self, ambient: np.ndarray|list[int|float]) -> None:
"""
Parameters
----------
ambient : np.ndarray | list[int | float]
The ambient color of the light (RGB).
Raises
------
Exception
If the shape of the dir argument is not (3,) and error is raised.
"""
if isinstance(ambient, np.ndarray) and ambient.shape != (3,):
raise Exception(f'Position attribute ambient must have a dimension of 3 got {ambient.shape} instead.')
if isinstance(ambient, list) and len(ambient) != 3:
raise Exception(f'Position attribute ambient must have a dimension of 3 got {len(ambient)} instead.')
self._ambient = np.array(ambient, dtype=np.float32)
@property
def diffuse(self) -> np.ndarray:
"""
The diffuse color of the light (RGB).
Returns
-------
np.ndarray
"""
return self._diffuse.copy()
@diffuse.setter
@blue.restrict
def diffuse(self, diffuse: np.ndarray|list[int|float]) -> None:
"""
Parameters
----------
diffuse : np.ndarray | list[int | float]
The diffuse color of the light (RGB).
Raises
------
Exception
If the shape of the dir argument is not (3,) and error is raised.
"""
if isinstance(diffuse, np.ndarray) and diffuse.shape != (3,):
raise Exception(f'Position attribute diffuse must have a dimension of 3 got {diffuse.shape} instead.')
if isinstance(diffuse, list) and len(diffuse) != 3:
raise Exception(f'Position attribute diffuse must have a dimension of 3 got {len(diffuse)} instead.')
self._diffuse = np.array(diffuse, dtype=np.float32)
@property
def specular(self) -> np.ndarray:
"""
The specular color of the light (RGB).
Returns
-------
np.ndarray
"""
return self._specular.copy()
@specular.setter
@blue.restrict
def specular(self, specular: np.ndarray|list[int|float]) -> None:
"""
Parameters
----------
specular : np.ndarray | list[int | float]
The specular color of the light (RGB).
Raises
------
Exception
If the shape of the dir argument is not (3,) and error is raised.
"""
if isinstance(specular, np.ndarray) and specular.shape != (3,):
raise Exception(f'Position attribute specular must have a dimension of 3 got {specular.shape} instead.')
if isinstance(specular, list) and len(specular) != 3:
raise Exception(f'Position attribute specular must have a dimension of 3 got {len(specular)} instead.')
self._specular = np.array(specular, dtype=np.float32)
@property
def directional(self) -> bool:
"""
The light is directional if this attribute is “true”, otherwise it is a spotlight.
Returns
-------
bool
"""
return self._directional
@directional.setter
@blue.restrict
def directional(self, directional: bool) -> None:
"""
Parameters
----------
directional : bool
The light is directional if this attribute is “true”, otherwise it is a spotlight.
"""
self._directional = directional
@property
def castshadow(self) -> bool:
"""
If this attribute is True the Light will cast shadows. More precisely, the geoms
illuminated by the light will cast shadows, however this is a property of Lights rather
than geoms. Since each shadow-casting light causes one extra rendering pass through all
geoms, this attribute should be used with caution.
Returns
-------
bool
"""
return self._castshadow
@castshadow.setter
@blue.restrict
def castshadow(self, castshadow: bool) -> None:
"""
Parameters
----------
castshadow : bool
If this attribute is True the Light will cast shadows. More precisely, the geoms
illuminated by the light will cast shadows, however this is a property of Lights rather
than geoms. Since each shadow-casting light causes one extra rendering pass through all
geoms, this attribute should be used with caution.
"""
self._castshadow = castshadow
@property
def active(self) -> bool:
"""
The light is active if this attribute is “true”. This can be used at runtime to turn
lights on and off.
Returns
-------
bool
"""
return self._active
@active.setter
@blue.restrict
def active(self, active: bool) -> None:
"""
Parameters
----------
active : bool
The light is active if this attribute is “true”. This can be used at runtime to turn
lights on and off.
"""
self._active = active
@property
def cutoff(self) -> float:
"""
Cutoff angle for spotlights in radians.
Returns
-------
float
"""
return self._cutoff
@cutoff.setter
@blue.restrict
def cutoff(self, cutoff: float|int) -> None:
"""
Parameters
----------
cutoff : float
Cutoff angle for spotlights in radians.
"""
self._cutoff = float(cutoff)
@property
def exponent(self) -> float:
"""
Exponent for spotlights. This setting controls the softness of the spotlight cutoff.
Returns
-------
float
"""
return self._exponent
@exponent.setter
@blue.restrict
def exponent(self, exponent: float|int) -> None:
"""
Parameters
----------
exponent : float
Exponent for spotlights. This setting controls the softness of the spotlight cutoff.
"""
self._exponent = float(exponent)