Source code for blueprints.assets

"""
.. 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}