"""
.. note::
It is recommended to not use assets directly. All property access is
handeled through a front facing ThingType. Assets are :mod:`UniqueThings <blueprints.thing.unique>`.
"""
import os
import struct
import xml.etree.ElementTree as xml
import numpy as np
import blueprints as blue
from blueprints.thing.colored import Color
from collections import defaultdict
from itertools import count
from imageio import imread, imwrite
ASSET_DIR = 'assets'
[docs]
class BaseAsset(blue.AssetType, blue.UniqueThing):
"""
Assets are used to link external resources to the model. Currently only :class:`MeshAssets <MeshAsset>`,
:class:`HFieldAssets <HFieldAsset>`, :class:`TextureAssets <TextureAsset>`,
:class:`MaterialAssets <MaterialAsset>` are supported, but ``SkinAssets`` and
``BoneAssets`` will be developed in a future version version.
"""
@property
def name(self) -> str:
"""
The user specified name might potentially be altered to avoid a naming conflict by appending an
enumeration scheme.
Returns
-------
str
"""
return self._name or f'asset_{self.ID}'
@name.setter
@blue.restrict
def name(self, name: str) -> None:
"""
The user specified name might potentially be altered to avoid a naming conflict by appending an
enumeration scheme.
Parameters
----------
name : str
The user specified name.
"""
self._name = name
[docs]
class MaterialAsset(blue.MaterialAssetType, BaseAsset, blue.thing.ColoredThing):
"""
See :mod:`Material <blueprints.material>`
Most attribute descriptions are partially taken from `Mujoco <https://mujoco.readthedocs.io/en/latest/XMLreference.html#asset-material>`__.
"""
[docs]
@blue.restrict
def __init__(self,
texture: blue.TextureType|None = None,
texrepeat: list[int|float]|np.ndarray = [1, 1],
texuniform: bool = False,
emission: int|float = 0.0,
specular: int|float = 0.5,
shininess: int|float = 0.5,
reflectance: int|float = 0.0,
metallic: int|float =-1.0,
roughness: int|float =-1.0,
color: str|object|None = None,
name: str|None = None):
self._built = False
# MUJOCO ATTRIBUTES
self.texrepeat = texrepeat
self.texuniform = texuniform
self.emission = emission
self.specular = specular
self.shininess = shininess
self.reflectance = reflectance
self.metallic = metallic
self.roughness = roughness
# TEXTURE
self.texture = texture
super().__init__(name=name)
if color is not None:
self.color = color
if name is None:
self._name = None
@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 Thing.
"""
if not self._built:
self._built = True
if self.texture is not None:
self._xml_root = xml.SubElement(world._xml_asset,
'material',
texture=self.texture.asset.name,
**self._mujoco_specs(kwargs))
else:
self._xml_root = xml.SubElement(world._xml_asset,
'material',
**self._mujoco_specs(kwargs))
#self._index = indicies['mesh']
#indicies['mesh'] += 1
if self.texture is not None:
self.texture._build(parent=parent,
world=world,
indicies=indicies,
**kwargs)
return self._xml_root
@property
def texrepeat(self) -> np.ndarray:
"""
The number of repetitions of the Texture in two directions
Returns
-------
np.ndarray
"""
return self._texrepeat.copy()
@texrepeat.setter
@blue.restrict
def texrepeat(self, texrepeat: list[int|float]|np.ndarray) -> None:
if isinstance(texrepeat, list):
texrepeat = np.array(texrepeat, dtype=np.float32)
self._texrepeat = texrepeat
@property
def texuniform(self) -> bool:
"""
For :class:`BoxTextures <blueprints.texture.Box>`, this attribute controls how
cube mapping is applied. The default value ``False`` means apply cube mapping
directly, using the actual size of the object. The value “true” maps the texture
to a unit object before scaling it to its actual size (geometric primitives are
created by the renderer as unit objects and then scaled). In some cases this
leads to more uniform texture appearance, but in general, which settings produces
better results depends on the texture and the object. For 2d textures, this attribute
interacts with texrepeat above. Let texrepeat be N. The default value “false” means
that the 2d texture is repeated N times over the (z-facing side of the) object.
The value “true” means that the 2D :class:`PlaneTexture <blueprints.texture.Plane>`
is repeated N times over one spatial unit, regardless of object size.
Returns
-------
bool
"""
return self._texuniform
@texuniform.setter
@blue.restrict
def texuniform(self, texuniform: bool) -> None:
self._texuniform = texuniform
@property
def emission(self) -> float:
"""
If set positive, the Material emitts lights.
Returns
-------
float
"""
return self._emission
@emission.setter
@blue.restrict
def emission(self, emission: int|float) -> None:
self._emission = float(emission)
@property
def specular(self) -> float:
"""
Specularity in OpenGL. This value should be in the range [0 1].
Returns
-------
float
"""
return self._specular
@specular.setter
@blue.restrict
def specular(self, specular: int|float) -> None:
self._specular = float(specular)
@property
def shininess(self) -> float:
"""
Shininess in OpenGL.
Returns
-------
float
"""
return self._shininess
@shininess.setter
@blue.restrict
def shininess(self, shininess: int|float) -> None:
self._shininess = float(shininess)
@property
def reflectance(self) -> float:
"""
This attribute should be in the range [0 1]. If the value
is greater than 0, and the material is applied to a plane
or a box geom, the renderer will simulate reflectance. The
larger the value, the stronger the reflectance. For boxes,
only the face in the direction of the local +Z axis is
reflective. Simulating reflectance properly requires
ray-tracing which cannot (yet) be done in real-time. We
are using the stencil buffer and suitable projections instead.
Only the first reflective geom in the model is rendered as
such. This adds one extra rendering pass through all geoms,
in addition to the extra rendering pass added by each
shadow-casting light.
Returns
-------
float
"""
return self._reflectance
@reflectance.setter
@blue.restrict
def reflectance(self, reflectance: int|float) -> None:
self._reflectance = float(reflectance)
@property
def metallic(self) -> float:
"""
This attribute corresponds to uniform metallicity
coefficient applied to the entire material. This
attribute has no effect in MuJoCo’s native renderer,
but it can be useful when rendering scenes with a
physically-based renderer. In this case, if a
non-negative value is specified, this metallic value
should be multiplied by the metallic texture sampled
value to obtain the final metallicity of the material.
Returns
-------
float
"""
return self._metallic
@metallic.setter
@blue.restrict
def metallic(self, metallic: int|float) -> None:
self._metallic = float(metallic)
@property
def roughness(self) -> float:
"""
This attribute corresponds to uniform roughness
coefficient applied to the entire material. This
attribute has no effect in MuJoCo’s native renderer,
but it can be useful when rendering scenes with a
physically-based renderer. In this case, if a non-negative
value is specified, this roughness value should be
multiplied by the roughness texture sampled value to
obtain the final roughness of the material.
Returns
-------
float
"""
return self._roughness
@roughness.setter
@blue.restrict
def roughness(self, roughness: int|float) -> None:
self._roughness = float(roughness)
[docs]
class TextureAsset(blue.TextureAssetType, BaseAsset):
"""
See :mod:`Texture <blueprints.texture>`
Most attribute descriptions are partially taken from `Mujoco <https://mujoco.readthedocs.io/en/latest/XMLreference.html#asset-texture>`__.
"""
[docs]
@blue.restrict
def __init__(self,
#color_space: str = 'auto',
content: str|None = None,
file: str|None = None,
grid_size: list[int] = [1, 1],
grid_layout: str|None = None,
fileright: str|None = None,
fileleft: str|None = None,
fileup: str|None = None,
filedown: str|None = None,
filefront: str|None = None,
fileback: str|None = None,
builtin: str|None = None,
color_1: object = [0.8, 0.8, 0.8],
color_2: object = [0.5, 0.5, 0.5],
mark: str|None = None,
color_mark: object = [0.0, 0.0, 0.0],
random: float = 0.01,
width: int = 0,
height: int = 0,
h_flip: bool = False,
v_flip: bool = False,
n_channel: int = 3,
name: str|None = None):
self._built = False
self._image = None
self._image_up = None
self._image_down = None
self._image_left = None
self._image_right = None
self._image_front = None
self._image_back = None
#self.color_space = color_space
self.content = content
self.file = file
self.grid_size = grid_size
self.grid_layout = grid_layout
self.fileright = fileright
self.fileleft = fileleft
self.fileup = fileup
self.filedown = filedown
self.filefront = filefront
self.fileback = fileback
self.builtin = builtin
self.color_1 = color_1
self.color_2 = color_2
self.mark = mark
self.color_mark = color_mark
self.random = random
self.width = width
self.height = height
self.h_flip = h_flip
self.v_flip = v_flip
self.n_channel = n_channel
super().__init__(name=name)
self._load_images()
if name is None:
self._name = None
@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 Thing.
"""
if not self._built:
# MAKE DIRECTORIES
assets_path = f'{world._path}/{ASSET_DIR}'
if not os.path.exists(assets_path):
os.mkdir(assets_path)
dirname = f'{world._path}/{ASSET_DIR}/textures'
if not os.path.exists(dirname):
os.mkdir(dirname)
# SAVE FILES
filenames = self._save_images(dirname)
# UPDATE FILENAMES
kwargs = kwargs.copy()
kwargs.update(filenames)
self._built = True
self._xml_root = xml.SubElement(world._xml_asset,
'texture',
**self._mujoco_specs(kwargs))
return self._xml_root
def _load_images(self):
if self.file is not None:
self._image = imread(self.file)
if self.fileup is not None:
self._imageup = imread(self.fileup)
if self.filedown is not None:
self._imagedown = imread(self.filedown)
if self.fileleft is not None:
self._imageleft = imread(self.fileleft)
if self.fileright is not None:
self._imageright = imread(self.fileright)
if self.filefront is not None:
self._imagefront = imread(self.filefront)
if self.fileback is not None:
self._imageback = imread(self.fileback)
def _save_images(self, dirname) -> dict:
files = dict()
if self.file is not None:
file = self._resolve_filename(dirname, self.file)
imwrite(file, self._image)
files['file'] = file
if self.fileup is not None:
fileup = self._resolve_filename(dirname, self.fileup)
imwrite(fileup, self._imageup)
files['fileup'] = fileup
if self.filedown is not None:
filedown = self._resolve_filename(dirname, self.filedown)
imwrite(filedown, self._imagedown)
files['filedown'] = filedown
if self.fileleft is not None:
fileleft = self._resolve_filename(dirname, self.fileleft)
imwrite(fileleft, self._imageleft)
files['fileleft'] = fileleft
if self.fileright is not None:
fileright = self._resolve_filename(dirname, self.fileright)
imwrite(fileright, self._imageright)
files['fileright'] = fileright
if self.filefront is not None:
filefront = self._resolve_filename(dirname, self.filefront)
imwrite(filefront, self._imagefront)
files['filefront'] = filefront
if self.fileback is not None:
fileback = self._resolve_filename(dirname, self.fileback)
imwrite(fileback, self._imageback)
files['fileback'] = fileback
return files
def _resolve_filename(self, dirname: str, filename: str, prefix=None):
pathname, basename = os.path.split(filename)
if not pathname.endswith(dirname):
if prefix is None:
return f'{dirname}/{self.ID}_{basename}'
else:
return f'{dirname}/{self.ID}_{prefix}_{basename}'
else:
return filename
# DERIVED ATTRIBUTES
@property
def type(self) -> str:
"""
Derived type attribute for Mujoco,
Returns
-------
str
"""
return self._TYPE
#@property
#def colorspace(self) -> str:
# return self.color_space
@property
def content_type(self) -> str|None:
"""
Content type (MIME type) taking values ``None``, ``'image/png'``,
``'image/ktx'`` and ``'image/vnd.mujoco.texture'``.
Returns
-------
str | None
"""
return self.content
@property
def gridsize(self) -> list[int]:
"""
Grid size attrigute ``[n_rows, n_cols]`` defining the grid.
For non-grid Textures this value is ignored.
Returns
-------
list[int]
"""
return np.array(self.grid_size, dtype=np.int8)
@property
def gridlayout(self) -> str|None:
"""
The layout of the grid. See example above.
Returns
-------
str | None
"""
return self.grid_layout
@property
def rgb1(self) -> np.ndarray:
"""
The first color of a procedural Texture.
Returns
-------
blue.ColorType
"""
return self.color_1.rgb
@property
def rgb2(self) -> np.ndarray:
"""
The second color of a procedural Texture.
Returns
-------
blue.ColorType
"""
return self.color_2.rgb
@property
def markrgb(self) -> np.ndarray:
"""
The mark color of a procedural Texture.
Returns
-------
blue.ColorType
"""
return self.color_mark.rgb
@property
def hflip(self) -> bool:
"""
If true, images loaded from file are flipped in the horizontal
direction. Does not affect procedural textures.
Returns
-------
bool
"""
return self.h_flip
@property
def vflip(self) -> bool:
"""
If true, images loaded from file are flipped in the vertical
direction. Does not affect procedural textures.
Returns
-------
bool
"""
return self.v_flip
@property
def nchannel(self) -> int:
"""
The number of channels in the texture image file. This allows
loading 4-channel textures (RGBA) or single-channel textures
(e.g., for Physics-Based Rendering properties such as roughness
or metallic).
Returns
-------
int
"""
return self.n_channel
# BLUEPRINTS ATTRIBUTES
#@property
#def color_space(self) -> str:
# return self._color_space
#@color_space.setter
#@blue.restrict
#def color_space(self, color_space: str) -> None:
# color_space = color_space.strip()
# if color_space not in ('auto', 'linear', 'sRGB'):
# raise ValueError(f'Attribute color_space must be set to "auto", "linear" or "sRGB". Got "{color_space}" instead.')
# self._color_space = color_space
@property
def content(self) -> str|None:
"""
Content type (MIME type) taking values ``None``, ``'image/png'``,
``'image/ktx'`` and ``'image/vnd.mujoco.texture'``.
Returns
-------
str | None
"""
return self._content
@content.setter
@blue.restrict
def content(self, content: str|None) -> None:
if isinstance(content, str):
content = content.strip()
if content is not None and content not in ('image/png', 'image/ktx', 'image/vnd.mujoco.texture'):
raise ValueError(f'Attribute content must be set to MIME type "image/png", "image/ktx" or "image/vnd.mujoco.texture". Got "{content}" instead.')
self._content = content
@property
def file(self) -> str|None:
"""
Name of the texture file.
Returns
-------
str | None
"""
return self._file
@file.setter
@blue.restrict
def file(self, file: str|None) -> None:
if isinstance(file, str):
file = file.strip()
self._file = file
@property
def grid_size(self) -> list[int]:
"""
Grid size attrigute ``[n_rows, n_cols]`` defining the grid.
For non-grid Textures this value is ignored.
Returns
-------
list[int]
"""
return [self._n_rows, self._n_cols]
@grid_size.setter
@blue.restrict
def grid_size(self, grid_size: list[int]) -> None:
if len(grid_size) != 2:
raise ValueError(f'grid_size must be a list of length 2. Got {len(grid_size)} instead.')
rows, cols = grid_size
if rows < 0:
raise ValueError(f'Rows in grid_size[0] must be positive. Got {rows} instead.')
if cols < 0:
raise ValueError(f'Columns in grid_size[1] must be positive. Got {cols} instead.')
if rows * cols > 12:
raise ValueError(f'Rows × Cols in grid_size must not multiply beyond 12. Got {rows} × {cols} = {rows * cols} instead.')
self._n_rows = rows
self._n_cols = cols
@property
def grid_layout(self) -> str|None:
"""
The layout of the grid. See example above.
Returns
-------
str | None
"""
return self._grid_layout
@grid_layout.setter
@blue.restrict
def grid_layout(self, grid_layout: str|None) -> None:
self._grid_layout = grid_layout
@property
def fileright(self) -> str|None:
"""
The file name for the right side of a :class:`Box`.
Returns
-------
str | None
"""
return self._fileright
@fileright.setter
@blue.restrict
def fileright(self, fileright: str|None) -> None:
if isinstance(fileright, str):
fileright = fileright.strip()
self._fileright = fileright
@property
def fileleft(self) -> str|None:
"""
The file name for the left side of a :class:`Box`.
Returns
-------
str | None
"""
return self._fileleft
@fileleft.setter
@blue.restrict
def fileleft(self, fileleft: str|None) -> None:
if isinstance(fileleft, str):
fileleft = fileleft.strip()
self._fileleft = fileleft
@property
def fileup(self) -> str|None:
"""
The file name for the upper side of a :class:`Box`.
Returns
-------
str | None
"""
return self._fileup
@fileup.setter
@blue.restrict
def fileup(self, fileup: str|None) -> None:
if isinstance(fileup, str):
fileup = fileup.strip()
self._fileup = fileup
@property
def filedown(self) -> str|None:
"""
The file name for the down side of a :class:`Box`.
Returns
-------
str | None
"""
return self._filedown
@filedown.setter
@blue.restrict
def filedown(self, filedown: str|None) -> None:
if isinstance(filedown, str):
filedown = filedown.strip()
self._filedown = filedown
@property
def filefront(self) -> str|None:
"""
The file name for the front side of a :class:`Box`.
Returns
-------
str | None
"""
return self._filefront
@filefront.setter
@blue.restrict
def filefront(self, filefront: str|None) -> None:
if isinstance(filefront, str):
filefront = filefront.strip()
self._filefront = filefront
@property
def fileback(self) -> str|None:
"""
The file name for the back side of a :class:`Box`.
Returns
-------
str | None
"""
return self._fileback
@fileback.setter
@blue.restrict
def fileback(self, fileback: str|None) -> None:
if isinstance(fileback, str):
fileback = fileback.strip()
self._fileback = fileback
@property
def builtin(self) -> str|None:
"""
If set, this attribute specifies a builtin procedural Texture.
See examples above.
Returns
-------
str | None
"""
return self._builtin
@builtin.setter
@blue.restrict
def builtin(self, builtin: str|None) -> None:
if isinstance(builtin, str):
builtin = builtin.strip()
if builtin is not None and builtin not in ('gradient', 'checker', 'flat'):
raise ValueError(f'Attribute builtin must be set to "gradient", "checker", "flat" or None. Got "{builtin}" instead.')
self._builtin = builtin
@property
def color_1(self) -> Color:
"""
The first color of a procedural Texture.
Returns
-------
blue.ColorType
"""
return self._color_1
@color_1.setter
@blue.restrict
def color_1(self, color_1: object) -> None:
self._color_1 = Color(color_1)
@property
def color_2(self) -> Color:
"""
The second color of a procedural Texture.
Returns
-------
blue.ColorType
"""
return self._color_2
@color_2.setter
@blue.restrict
def color_2(self, color_2: object) -> None:
self._color_2 = Color(color_2)
@property
def mark(self) -> str|None:
"""
If set this attribute specifies additional markings on
a procedural Texture. Possible values are ``None``,
``'edge'``, ``'cross'`` and ``'random'``. See examples above.
Returns
-------
str | None
"""
return self._mark
@mark.setter
@blue.restrict
def mark(self, mark: str|None) -> None:
if isinstance(mark, str):
mark = mark.strip()
if mark is not None and mark not in ('edge', 'cross', 'random'):
raise ValueError(f'Attribute mark must be set to "edge", "cross", "random" or None. Got "{mark}" instead.')
self._mark = mark
@property
def color_mark(self) -> Color:
"""
The mark color of a procedural Texture.
Returns
-------
blue.ColorType
"""
return self._color_mark
@color_mark.setter
@blue.restrict
def color_mark(self, color_mark: object) -> None:
self._color_mark = Color(color_mark)
@property
def random(self) -> float:
"""
If the ``mark`` attribute is set to ``'random'``, this
attribute specifies the probability of a pixel in the
texture being the ``mark_color``.
Returns
-------
float
"""
return self._random
@random.setter
@blue.restrict
def random(self, random: float) -> None:
if not 0 <= random <= 1:
raise ValueError(f'Attribute random specifies a probability and must lay within the interval [0, 1]. Got {random} instead.')
self._random = random
@property
def width(self) -> int:
"""
The width of a procedural texture, i.e., the number of columns
in the image. Larger values usually result in higher quality
images, although in some cases (e.g. checker patterns) small
values are sufficient. For textures loaded from files, this
attribute is ignored.
Returns
-------
int
"""
return self._width
@width.setter
@blue.restrict
def width(self, width: int) -> None:
if width < 0:
raise ValueError(f'Attribute width must be positive. Got {width} instead.')
self._width = width
@property
def height(self) -> int:
"""
The height of the procedural texture, i.e., the number of rows
in the image. For :class:`Box` and :class:`Skybox` textures, this attribute is
ignored and the height is set to 6 times the width. For textures
loaded from files, this attribute is ignored.
Returns
-------
int
"""
return self._height
@height.setter
@blue.restrict
def height(self, height: int) -> None:
if height < 0:
raise ValueError(f'Attribute height must be positive. Got {height} instead.')
self._height = height
@property
def n_channel(self) -> int:
"""
The number of channels in the texture image file. This allows
loading 4-channel textures (RGBA) or single-channel textures
(e.g., for Physics-Based Rendering properties such as roughness
or metallic).
Returns
-------
int
"""
return self._n_channel
@n_channel.setter
@blue.restrict
def n_channel(self, n_channel: int) -> None:
if n_channel < 0:
raise ValueError(f'Attribute n_channel must be positive. Got {n_channel} instead.')
self._n_channel = n_channel
@property
def h_flip(self) -> bool:
"""
If true, images loaded from file are flipped in the horizontal
direction. Does not affect procedural textures.
Returns
-------
bool
"""
return self._h_flip
@h_flip.setter
@blue.restrict
def h_flip(self, h_flip: bool) -> None:
self._h_flip = h_flip
@property
def v_flip(self) -> bool:
"""
If true, images loaded from file are flipped in the vertical
direction. Does not affect procedural textures.
Returns
-------
bool
"""
return self._v_flip
@v_flip.setter
@blue.restrict
def v_flip(self, v_flip: bool) -> None:
self._v_flip = v_flip
[docs]
class MeshAsset(blue.MeshAssetType, BaseAsset, blue.thing.MoveableThing):
"""
MeshAssets handle access to the data attributes used to define the :class:`Mesh <blueprints.geoms.Mesh>`.
All vertex, face, normals and texture coordinate data are stored in the MeshAssets :class:`MeshCache <blueprints.cache.MeshCache>`
in the :attr:`cache` attribute.
In general, all Mesh data that are structured for xml representation and insufficient for user
manipulation are named as singular :attr:`vertex`, :attr:`face`, :attr:`texcoord` and :attr:`normal`
while their plural form :attr:`vertecies`, :attr:`faces`, :attr:`cache.texcoords <blueprints.cache.MeshCache.texcoords>` and :attr:`cache.normals <blueprints.cache.MeshCache.normals>` are used
for user interpretable representations and manipulation.
.. note::
The position of MeshAssets is defined in mujoco via the :attr:`refpos` attribute, which differs
to the standard ``pos`` attribute in that it defines the reference frames relative position w.r.t.
to its position instead of the vice versa the relative position w.r.t. the reference frames
origin, effectively making ``refpos = - pos``. We find this to be unnecessarily confusing and
opted to implement the standard position attribute only translating when the kinematic tree gets
converted to Mujoco xml. It is highly recommended to use the :attr:`pos` argument to modify the
MeshAssets position.
Attributes
----------
built : bool
A flag indicating whether the MeshAssets has been built. This is necessary since multiple
:class:`Meshes <blueprints.geoms.Mesh>` might use the same :class:`MeshAsset` in which case
it should be built only once.
cache : blue.CacheType
The :class:`MeshCache <blueprints.cache.MeshCache>` which stores the Meshs data.
filename : str
The filename from which the Mesh data is loaded or to which it will be saved.
xml_data : bool
A flag indicating whether the Mesh data is written into the xml directly or into and external
file which is then linked to in the xml asset element.
"""
[docs]
@blue.restrict
def __init__(self,
pos: np.ndarray|list[int|float] = [0., 0., 0.],
vertecies: np.ndarray|list[np.ndarray|list[float|int]]|None = None,
faces: list[np.ndarray|list[float|int]]|None = None,
scale: np.ndarray|list[int|float] = [1., 1., 1.],
filename: str|None = None,
centered: bool = False,
xml_data: bool = False,
cache: blue.ThingType|None = None,
name: str|None = None,
**kwargs) -> None:
"""
Parameters
----------
pos : np.ndarray | list[int | float], optional
pos : np.ndarray | list[int | float], optional
Represents the position of the Asset. Changing this attribute also changes the properties
:attr:`x`, :attr:`y` and :attr:`z`.
vertecies : np.ndarray | list[np.ndarray | list[float | int]] | None, optional
A list of all vertex positions. The vertecies are stored in :attr:`cache`.
faces : list[np.ndarray | list[float | int]] | None, optional
A list of all faces. A face consists of at least 3 indecies of vertecies. The faces are
stored in :attr:`cache`.
scale : np.ndarray | list[int | float], optional
The scale is a 1D array like object of length 3. Each component is used to scale the X,
Y and Z-axis respectively. Changing the scale is computationally cheap since it does not
change the vertecies or other data in :attr:`cache`.
filename : str | None, optional
The filename from which the mesh data is loaded and to to which the mesh will be saved.
The file is saved in a special directory such that no input file is overwritten by the
output file.
centered : bool, optional
If ``True`` the vertecies are normalized such that their mean position is the reference
frames origin.
xml_data : bool, optional
If ``True`` the Mesh data is not written to an external file and linked to in the xml,
but written into the xml directly. It is recommended to only use this option if the Mesh
data is small enough to not blot the xml.
cache : blue.ThingType | None, optional
The :class:`Cache <blueprints.cache.MeshCache>` which stores the Mesh data.
name : str | None, optional
The user specified name might potentially be altered to avoid a naming conflict by
appending an enumeration scheme.
**kwargs
Keyword arguments are passed to ``super().__init__``.
"""
if cache is None:
self.cache = blue.cache.MeshCache(vertecies=vertecies,
faces=faces,
filename=filename,
centered=centered)
else:
self.cache = cache
self.cache._add(self)
self.xml_data = xml_data
#self.filename = filename
self.scale = scale
self._built = False
super().__init__(pos=pos,
name=name,
**kwargs)
#blue.REGISTER.assets.append(self)
if name is None:
self._name = None
[docs]
@blue.restrict
def load(self, filename: str) -> None:
"""
This methods loads the mesh data from the filename to the assets cache.
Parameters
----------
filename : str
The filename must be binary or ascii for OBJ files or binary for STL files.
"""
self.cache.load(filename)
[docs]
@blue.restrict
def save(self, filename: str) -> None:
"""
This method saves the mesh data from the cache to the filename.
Parameters
----------
filename : str
The filename must either end with the ``'.obj`` or ``'.stl'`` file extension. Both file
formats are saved as binary files.
"""
self.cache.save(filename)
@blue.restrict
@classmethod
def _from_xml_element(cls,
xml_element: xml.Element,
cache: blue.CacheType) -> blue.ThingType:
"""
This method reconstructs an MeshAsset from an xml element.
Parameters
----------
xml_element : xml.Element
The xml element from which a MeshAsset is reconstructed.
cache : blue.CacheType
The Cache which stores the Mesh data.
Returns
-------
blue.ThingType
The reconstructed MeshAsset.
"""
init_args, post_args, rest_args = cls._xml_element_args(xml_element)
init_args['cache'] = cache
if 'refpos' in post_args:
init_args['pos'] = - post_args.pop('refpos')
if 'vertex' in rest_args:
init_args['xml_data'] = True
init_args['vertecies'] = rest_args.pop('vertex')
else:
init_args['xml_data'] = False
mesh = object.__new__(cls)
mesh.__init__(**init_args)
for key, val in post_args.items():
setattr(mesh, key, val)
return mesh
def _build(self, parent, world, indicies, **kwargs):
"""
This method is called by the Assets parent to construct the xml representations of
the kinematic tree.
Parameters
----------
parent : xml.etree.ElementTree.Element
The xml element of its parent
world : WorldType
The World from which the build method was called initially
**kwargs
Keyword arguments are overwrite the mujoco specs for building.
Returns
-------
xml.etree.ElementTree.Element
The built xml element of the Asset.
"""
if not self._built:
if self.xml_data:
mujoco_specs = self._mujoco_specs()
mujoco_specs.update(self.cache._mujoco_specs())
self._xml_root = xml.Element('mesh', **mujoco_specs)
self.cache._ROOT = world
else:
# MAKE DIRECTORIES
assets_path = f'{world._path}/{ASSET_DIR}'
if not os.path.exists(assets_path):
os.mkdir(assets_path)
dirname = f'{world._path}/{ASSET_DIR}/meshes'
if not os.path.exists(dirname):
os.mkdir(dirname)
# BUILD CACHE AND SELF
self.cache._build(dirname)
self._xml_root = xml.Element('mesh',
file=self.cache._path,
**self._mujoco_specs({'vertex': None, 'face': None}))
self._index = indicies['mesh']
self._built = True
if self._xml_root not in world._xml_asset:
world._xml_asset.append(self._xml_root)
return self._xml_root
# MUJOCO PROPERTIES
@property
def file(self) -> str | None:
"""
Derived dummy variable for Mujoco XML
Returns
-------
str
"""
return self.filename
@property
def filename(self) -> str | None:
"""
The filename of the Mesh
Returns
-------
str
"""
return self.cache.filename
@filename.setter
@blue.restrict
def filename(self, filename: str | None) -> None:
"""
Returns
-------
str
"""
self.cache.filename = filename
@property
def refpos(self) -> np.ndarray:
"""
The negative :attr:`pos`. This attribute is implemented for mujoco, but synchronized with
:attr:`pos` which is recommended to to be used instead.
Returns
-------
np.ndarray
"""
return - self.pos
@property
@blue.restrict
def vertex(self) -> np.ndarray:
"""
This attribute is used to write construct the mujoco xml. It returns a flattened list of
coordinates that is written raw to xml. For a structured list of vertecies see :attr:`vertecies`.
Returns
-------
np.ndarray
"""
return self.cache.vertex
@property
@blue.restrict
def face(self) -> np.ndarray|None:
"""
This attribute is used to write to xml. It returns a flattened list of indecies that is
written raw to xml. For a structured list of faces see :attr:`faces`.
Returns
-------
np.ndarray | None
"""
return self.cache.face
@face.setter
@blue.restrict
def face(self, face: list) -> None:
"""
Parameters
----------
face : list
This attribute is used to write to xml. It returns a flattened list of indecies that is
written raw to xml. For a structured list of faces see :attr:`faces`.
"""
self.cache._prepare_for_modification(self)
self.cache.face = face
@property
def texcoord(self) -> np.ndarray|None:
"""
This attribute is used to write to xml. It returns a flattened list of texture coordinates that
is written raw to xml. For a structured list of texture coordinates see :attr:`texcoords`.
Returns
-------
np.ndarray | None
"""
return self.cache.texcoord
@texcoord.setter
@blue.restrict
def texcoord(self, texcoord: list) -> None:
"""
Parameters
----------
texcoord : list
This attribute is used to write to xml. It returns a flattened list of texture
coordinates that is written raw to xml. For a structured list of texture coordinates see
:attr:`texcoords`.
"""
self.cache._prepare_for_modification(self)
self.cache.texcoord = texcoord
@property
def normal(self) -> np.ndarray|None:
"""
This attribute is used to write to xml. It returns a flattened list of face normals that is
written raw to xml. For a structured list of face normals see :attr:`normals`.
Returns
-------
np.ndarray | None
"""
return self.cache.normal
@normal.setter
@blue.restrict
def normal(self, normal: list) -> None:
"""
Parameters
----------
normal : list
This attribute is used to write to xml. It returns a flattened list of face normals that
is written raw to xml. For a structured list of face normals see :attr:`normals`.
"""
self.cache._prepare_for_modification(self)
self.cache.normal = normal
@property
def size(self) -> np.ndarray:
"""
The size attribute specifies the ranges for each axis that the Mesh occupies.
Returns
-------
np.ndarray
"""
return self.scale * self.cache.size
@size.setter
@blue.restrict
def size(self, size: np.ndarray|list[int|float]) -> None:
"""
The size attribute specifies the ranges for each axis that the Mesh occupies.
Parameters
----------
size : np.ndarray | list[int | float]
Changing this parameter scales the vertecies directly in the :class:`Cache <blueprints.cache.MeshCache>`
which is computationally costly. To scale the size of the Mesh independently from the
vertecies in a computationally cheap manner use the :attr:`scale` attribute.
"""
self.cache._prepare_for_modification(self)
size = np.array(size, dtype=np.float32) / self.scale
size[np.isinf(size)] = 0
self.cache.size = size
@property
def scale(self) -> np.ndarray:
"""
The scale is a 1D array like object of length 3. Each component is used to scale the X, Y and
Z-axis respectively. Changing the scale is computationally cheap since it does not change the
vertecies or other data in :attr:`cache`.
Returns
-------
np.ndarray
"""
return self._scale
@scale.setter
@blue.restrict
def scale(self, scale: np.ndarray|list[int|float]):
"""
Parameters
----------
scale : np.ndarray | list[int | float]
The scale is a 1D array like object of length 3. Each component is used to scale the X,
Y and Z-axis respectively. Changing the scale is computationally cheap since it does not
change the vertecies or other data in :attr:`cache`.
Raises
------
ValueError
If the scale contains a component that is 0 an error is raised.
"""
scale = np.array(scale, dtype=np.float32)
if np.any(scale == 0):
raise ValueError(f'A meshes scale is not allowed to be zero in any component got {scale}.')
self._scale = scale
# BLUEPRINTS PROPERTIES
@property
def vertecies(self) -> list[np.ndarray]:
"""
The list of vertecies. Each vertex is a np.ndarray with 3 components for each of the
spatial dimensions.
Returns
-------
list[np.ndarray]
"""
return self.cache.vertecies
@vertecies.setter
@blue.restrict
def vertecies(self, vertecies: np.ndarray|list[np.ndarray|list[float|int]]) -> None:
"""
Parameters
----------
vertecies : np.ndarray | list[np.ndarray | list[float | int]]
The list of vertecies. Each vertex is a np.ndarray with 3 components for each of the
spatial dimensions.
"""
self.cache._prepare_for_modification(self)
self.cache.vertecies = vertecies
@property
def faces(self) -> list[np.ndarray]|None:
"""
The list of faces. Each face is a np.ndarray with n indecies for each of the vertecies of the
face.
Returns
-------
list[np.ndarray] | None
"""
return self.cache.faces
@faces.setter
@blue.restrict
def faces(self, faces: np.ndarray|list[np.ndarray|list[float|int]]) -> None:
"""
Parameters
----------
faces : np.ndarray | list[np.ndarray | list[float | int]]
The list of faces. Each face is a np.ndarray with n indecies for each of the vertecies
of the face.
"""
self.cache._prepare_for_modification(self)
self.cache.faces = faces
[docs]
class HFieldAsset(blue.HFieldAssetType, BaseAsset, blue.thing.MoveableThing):
"""
See :class:`HField <blueprints.geoms.HField>`
Most attribute descriptions are partially taken from `Mujoco <https://mujoco.readthedocs.io/en/latest/XMLreference.html#body-geom>`__.
"""
[docs]
@blue.restrict
def __init__(self,
filename: str|None = None,
pos: np.ndarray|list[int|float] = [0., 0., 0.],
terrain: np.ndarray|list[int|float]|None = None,
x_length: int|float|None = 1,
y_length: int|float|None = 1,
z_length: int|float|None = 1,
height_offset: int|float|None = 1,
xml_data: bool = False,
cache: blue.ThingType|None = None,
name: str|None = None,
**kwargs) -> None:
if cache is None:
self.cache = blue.cache.HFieldCache(terrain=terrain,
filename=filename)
else:
self.cache = cache
self.cache._add(self)
self.xml_data = xml_data
self.filename = filename
self.x_length = x_length
self.y_length = y_length
self.z_length = z_length
self.height_offset = height_offset
self._built = False
super().__init__(pos=pos,
name=name,
**kwargs)
if name is None:
self._name = None
@blue.restrict
def _build(self, parent, world, indicies, **kwargs):
"""
This method is called by the Assets parent to construct the xml representations of
the kinematic tree.
Parameters
----------
parent : xml.etree.ElementTree.Element
The xml element of its parent
world : WorldType
The World from which the build method was called initially
**kwargs
Keyword arguments are overwrite the mujoco specs for building.
Returns
-------
xml.etree.ElementTree.Element
The built xml element of the Asset.
"""
if not self._built:
if self.xml_data and False:
mujoco_specs = self._mujoco_specs()
mujoco_specs.update(self.cache._mujoco_specs())
self._xml_root = xml.Element('hfield', **mujoco_specs)
else:
# MAKE DIRECTORIES
if not os.path.exists(ASSET_DIR):
os.mkdir(ASSET_DIR)
assets_path = f'{world._path}/{ASSET_DIR}/hfields'
if not os.path.exists(assets_path):
os.mkdir(assets_path)
# BUILD CACHE AND SELF
self.cache._build(assets_path)
self._xml_root = xml.Element('hfield',
file=self.cache._path,
**self._mujoco_specs({'elevation': None}))
self.cache._world = world
self.cache._index = indicies['hfield']
indicies['hfield'] += 1
self._built = True
if self._xml_root not in world._xml_asset:
world._xml_asset.append(self._xml_root)
self.cache._ROOT = world
return self._xml_root
@blue.restrict
@classmethod
def _from_xml_element(cls,
xml_element: xml.Element,
cache: blue.CacheType) -> blue.ThingType:
init_args, post_args, rest_args = cls._xml_element_args(xml_element)
init_args['cache'] = cache
# size is a read-only derived property; convert to init args
size = post_args.pop('size', None)
if size is not None:
init_args['x_length'] = float(size[0]) * 2
init_args['y_length'] = float(size[1]) * 2
init_args['z_length'] = float(size[2]) * 2
init_args['height_offset'] = float(size[3]) * 2
obj = object.__new__(cls)
obj.__init__(**init_args)
for key, val in post_args.items():
setattr(obj, key, val)
return obj
def __getitem__(self,
key: tuple[slice|int]) -> np.ndarray|np.float32:
"""
Returns
-------
np.ndarray | np.float32
The values from the selected index/slice of the height field.
"""
return self.cache[key]
def __setitem__(self,
key: tuple[slice|int],
value: int|float|list[int|float]|np.ndarray) -> None:
"""
Parameters
----------
key : tuple[slice | int]
The indecies/slices of acces
value : int|float|list[int|float]|np.ndarray
The value to be assigned in the selected parts of the field.
"""
self.cache._prepare_for_modification(self)
self.cache[key] = value
# MUJOCO PROPERTIES
@property
def size(self) -> np.ndarray:
"""
Derived size attribute for Mujoco XML.
Returns
-------
np.ndarray
"""
return np.array([self.x_length/2, self.y_length/2, self.z_length/2, self.height_offset/2])
@property
def name(self) -> str:
"""
The user specified name might potentially be altered to avoid a naming conflict by appending an
enumeration scheme.
Returns
-------
str
"""
return self._name or f'asset_{self.ID}'
@name.setter
@blue.restrict
def name(self, name: str) -> None:
"""
The user specified name might potentially be altered to avoid a naming conflict by appending an
enumeration scheme.
Parameters
----------
name : str
The user specified name.
"""
self._name = name
# BLUEPRINTS PROPERTIES
@property
def x_length(self):
"""
Length along the ``X``-axis of the HField.
Returns
-------
float
"""
return self._x_length
@property
def y_length(self):
"""
Length along the ``Y``-axis of the HField.
Returns
-------
float
"""
return self._y_length
@property
def z_length(self):
"""
Length along the ``Z``-axis of the HField.
Returns
-------
float
"""
return self._z_length
@property
def height_offset(self):
"""
Height offset along the ``Z``-axis of the HField.
Returns
-------
float
"""
return self._height_offset
@x_length.setter
@blue.restrict
def x_length(self, x_length: int|float|np.int32|np.int64|np.float32|np.float64) -> None:
self._x_length = float(x_length)
@y_length.setter
@blue.restrict
def y_length(self, y_length: int|float|np.int32|np.int64|np.float32|np.float64) -> None:
self._y_length = float(y_length)
@z_length.setter
@blue.restrict
def z_length(self, z_length: int|float|np.int32|np.int64|np.float32|np.float64) -> None:
self._z_length = float(z_length)
@height_offset.setter
@blue.restrict
def height_offset(self, height_offset: int|float|np.int32|np.int64|np.float32|np.float64) -> None:
self._height_offset = float(height_offset)
@property
def terrain(self) -> list[np.ndarray]:
"""
The terrain data of the HField
Returns
-------
list[np.ndarray]
"""
return self.cache.terrain
@terrain.setter
@blue.restrict
def terrain(self, terrain: np.ndarray|list[np.ndarray|list[float|int]]) -> None:
"""
Parameters
----------
vertecies : np.ndarray | list[np.ndarray | list[float | int]]
"""
self.cache._prepare_for_modification(self)
self.cache.terrain = terrain
ASSET_THINGS = {'mesh': MeshAsset,
'hfield': HFieldAsset}