Source code for blueprints.texture

"""
There are currently three types of textures supported in blueptints.
Boxes, 2D-Planes and Meshtextures. To apply a texture to a Thing it has 
to be mediated through a :class:`Material <blueprints.material.Material>`.

.. code-block::
	:caption: Texture asssignment

	>>> texture  = blue.texture.Plane()
	>>> material = blue.Material(texture=texture)
	>>> plane    = blue.geoms.Plane(material=material)


Texture Types
=============
We will present the basic Texture Types here for image textures. The 
same types are also usable for Procedural Textures (see below).

Plane
-----

The :class:`Plane` texture encodes 2D images which can be provided by a file path. 
For clarity the MIME type and the number of channels can be made explicit with 
``content`` and ``n_channel``. To flip the image vertically or horizontally 
``v_flip`` and ``h_flip`` can be used.

.. code-block::
	:caption: Plane Texture

	>>> sand_tex = blue.texture.Plane(filename='sand.png')
	>>> sand_mat = blue.Material(texture=sand_tex)
	>>> blue.geoms.Plane(material=sand_mat)

.. image:: /_static/texture_plane.png

Box
---

There are two ways of specifying the six sides of a :class:`Box` we give the first 
as an example here and the second for the equivalent :class:`Skybox` texture.
The provided file paths are as always taken relative to the file instantiating the World.

.. code-block::
	:caption: Box Texture

	>>> grass_tex = blue.texture.Box(filename_up   ='grass_top.png', 
	>>> 				 filename_left ='grass_side.png', 
	>>> 				 filename_right='grass_side.png', 
	>>> 				 filename_front='grass_side.png', 
	>>> 				 filename_back ='grass_side.png', 
	>>> 				 filename_down ='grass_bottom.png')
	>>> grass_mat = blue.Material(name='grass', texture=grass_tex)
	>>> grass = blue.geoms.Box(material=grass_mat)

.. image:: /_static/texture_box.png


Mesh
----

If a :class:`Mesh <blueprints.geoms.Mesh>` has texture coordinates (like ``.obj`` files) 
a :class:'Plane' texture can be applied to it. If a mesh does not have texture coordinates 
they can also be provided manually from the user through :attr:`Mesh.texcoord <blueprints.assets.MeshAsset.texcoord>`.

.. code-block::
	:caption: Mesh Texture

	>> creeper_tex = blue.texture.Plane(filename='creeper.png')
	>> creeper_mat = blue.Material(texture=creeper_tex)
	>> creeper = blue.geoms.Mesh(filename='creeper.obj')

.. image:: /_static/texture_mesh.png

SkyBox
------

Instead of applying a :class:`Skybox` texture via a :class:`Material <blueprints.material.Material>` 
it can be applied directly to the :class:`World <blueprints.world.World>` through the :attr:`World.texture <blueprints.world.World.texture>` 
attribute. Skybox and Box textures use equvalent attributes and constructions.

.. warning::
	As of Mujoco 3.3.2 there is a bug that prevents a skybox from being loaded if no other textured :class:`Material <blueprints.material.Material>` 
	if included in the :class:`World <blueprints.world.World>`.

To load a cube like texture from a single file it is partitioned into a grid through the 
``grid_size`` argument. The number of cells (i.e. the product of ``grid_size``) must not 
exceed 12. The assignment of each cell to a side of the cube is then specified via the 
``grid_layout`` string. Each row is read left to right with the character,
``'U'`` up, ``'D'`` down, ``'L'`` left, ``'R'`` right, ``'F'`` front, ``'B'`` back and ``'.'`` skip 
mapping sides to grid cells.

.. image:: /_static/bluesky_grid.png

.. code-block::
	:caption: Skybox Texture

	>>> sky = blue.texture.Skybox(filename='sky.png', 
	>>> 			      grid_layout='.U..LFRB.D..', 
	>>> 			      grid_size=[3, 4])
	>>> world.texture = sky

.. image:: /_static/texture_sky.png


Procedural Textures
===================
Mujoco supports simple procedurally generated textures which are accessable 
through the bultin attribute. Their apperance for :class:`Box Textures <blueprints.texture.Box>` 
and :class:`Plane Textures <blueprints.texture.Plane>` differs so we present both in the 
following examples.

.. note::
	Though they are named identically and we present Box Geoms with Box Textures 
	and Plane geoms with Plane Textures, the opposite assignment is also possible.


.. code-block::
	:caption: Example

	>>> box_tex = ...
	>>> plane_tex = ...
	>>> ...
	>>> box_mat = blue.Material(texture=box_tex)
	>>> box = blue.geoms.Box(material=box_mat, 
	>>> 			 x=-1.5,
	>>> 			 z=0.5, 
	>>> 			 gamma=TAU/8)
	>>> plane_mat = blue.Material(texture=plane_tex, 
	>>> 			      texrepeat=[2, 1])
	>>> plane = blue.geoms.Plane(material=plane_mat, 
	>>> 			     x_length=2, 
	>>> 			     y_length=1,
	>>> 			     x=1.5, 
	>>> 			     z=0.5)
	>>> world.attach(box, plane, blue.geoms.Plane(color='orange'))


Gradient
--------

.. code-block::
	:caption: Gradient Builtin Texture

	>>> box_tex = blue.texture.Box(builtin='gradient', 
	>>> 			       color_1='white', 
	>>> 			       color_2='black', 
	>>> 			       width=1000, 
	>>> 			       height=1000)
	>>> plane_tex = blue.texture.Plane(builtin='gradient',
	>>> 				   color_1='white', 
	>>> 				   color_2='black', 
	>>> 				   width=1000, 
	>>> 				   height=1000)
	>>> ...


.. image:: /_static/texture_gradient.png


Checker
-------

.. code-block::
	:caption: Checker Builtin Texture

	>>> box_tex = blue.texture.Box(builtin='checker', 
	>>> 			       color_1='white', 
	>>> 			       color_2='black', 
	>>> 			       width=1000, 
	>>> 			       height=1000)
	>>> plane_tex = blue.texture.Plane(builtin='checker',
	>>> 				   color_1='white', 
	>>> 				   color_2='black', 
	>>> 				   width=1000, 
	>>> 				   height=1000)
	>>> ...


.. image:: /_static/texture_checker.png

Flat
----

.. code-block::
	:caption: Flat Builtin Texture

	>>> box_tex = blue.texture.Box(builtin='flat', 
	>>> 			       color_1='white', 
	>>> 			       color_2='black', 
	>>> 			       width=1000, 
	>>> 			       height=1000)
	>>> plane_tex = blue.texture.Plane(builtin='flat',
	>>> 				p   color_1='white', 
	>>> 				   color_2='black', 
	>>> 				   width=1000, 
	>>> 				   height=1000)
	>>> ...


.. image:: /_static/texture_flat.png


Mark
----

Additionally there are three variations of markings: ``'edge'``, ``'cross'`` and ``'random'``.
The last marking takes the additional argument ``random`` indicating the probability of a texture 
pixel being colored with the ``color_mark``.

.. code-block::
	:caption: Mark Texture

	>>> edge_tex = blue.texture.Box(builtin='flat', 
	>>> 				color_1='white', 
	>>> 				color_2='black', 
	>>> 				color_mark='orange', 
	>>> 				mark='edge', 
	>>> 				width=50, 
	>>> 				height=50)
	>>> cross_tex = blue.texture.Box(builtin='flat', 
	>>> 				 color_1='white', 
	>>> 				 color_2='black', 
	>>> 				 color_mark='orange', 
	>>> 				 mark='cross', 
	>>> 				 width=50, 
	>>> 				 height=50)
	>>> random_1_tex = blue.texture.Box(builtin='flat', 
	>>> 				    color_1='white', 
	>>> 				    color_2='black', 
	>>> 				    color_mark='orange', 
	>>> 				    mark='random', 
	>>> 				    random=0.05,
	>>> 				    width=50, 
	>>> 				    height=50)
	>>> random_2_tex = blue.texture.Box(builtin='flat', 
	>>> 				    color_1='white', 
	>>> 				    color_2='black', 
	>>> 				    color_mark='orange', 
	>>> 				    mark='random', 
	>>> 				    random=0.5,
	>>> 				    width=50, 
	>>> 				    height=50)


.. image:: /_static/texture_mark.png

"""


import numpy as np
import blueprints as blue
from blueprints.thing.colored import Color



[docs] class BaseTexture(blue.BaseThing): """ Most attribute descriptions are partially taken from `Mujoco <https://mujoco.readthedocs.io/en/latest/XMLreference.html#asset-texture>`__. """ @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. """ self.asset._build(parent=parent, world=world, indicies=indicies, **kwargs) return self.asset._xml_root @property def asset(self) -> blue.TextureAssetType: """ The attributes of the texture are stored in a separate asset object. If this texture is assign to multiple objects, and then gets modified through ``Material.texture.attribute = value`` all other Materials referenceing the Texture will be unaltered. See :class:`UniqueThing <blueprints.thing.unique.UniqueThing>`. Returns ------- blue.TextureAssetType """ return self._asset @asset.setter @blue.restrict def asset(self, asset: blue.TextureAssetType) -> None: self._asset = asset # BLUEPRINT PROPERTIES @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.asset.content @content.setter @blue.restrict def content(self, content: str|None) -> None: self.asset._prepare_for_modification(self) self.asset.content = content @property def filename(self) -> str|None: """ Name of the texture file. Returns ------- str | None """ return self.asset.file @filename.setter @blue.restrict def filename(self, filename: str|None) -> None: self.asset._prepare_for_modification(self) self.asset.file = filename @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.asset.grid_size @grid_size.setter @blue.restrict def grid_size(self, grid_size: list[int]) -> None: self.asset._prepare_for_modification(self) self.asset.grid_size = grid_size @property def grid_layout(self) -> str|None: """ The layout of the grid. See example above. Returns ------- str | None """ return self.asset.grid_layout @grid_layout.setter @blue.restrict def grid_layout(self, grid_layout: str|None) -> None: self.asset._prepare_for_modification(self) self.asset.grid_layout = grid_layout @property def filename_right(self) -> str|None: """ The file name for the right side of a :class:`Box`. Returns ------- str | None """ return self.asset.filename_right @filename_right.setter @blue.restrict def filename_right(self, filename_right: str|None) -> None: self.asset._prepare_for_modification(self) self.asset.filename_right = filename_right @property def filename_left(self) -> str|None: """ The file name for the left side of a :class:`Box`. Returns ------- str | None """ return self.asset.filename_left @filename_left.setter @blue.restrict def filename_left(self, filename_left: str|None) -> None: self.asset._prepare_for_modification(self) self.asset.filename_left = filename_left @property def filename_up(self) -> str|None: """ The file name for the upper side of a :class:`Box`. Returns ------- str | None """ return self.asset.filename_up @filename_up.setter @blue.restrict def filename_up(self, filename_up: str|None) -> None: self.asset._prepare_for_modification(self) self.asset.filename_up = filename_up @property def filename_down(self) -> str|None: """ The file name for the down side of a :class:`Box`. Returns ------- str | None """ return self.asset.filename_down @filename_down.setter @blue.restrict def filename_down(self, filename_down: str|None) -> None: self.asset._prepare_for_modification(self) self.asset.filename_down = filename_down @property def filename_front(self) -> str|None: """ The file name for the front side of a :class:`Box`. Returns ------- str | None """ return self.asset.filename_front @filename_front.setter @blue.restrict def filename_front(self, filename_front: str|None) -> None: self.asset._prepare_for_modification(self) self.asset.filename_front = filename_front @property def filename_back(self) -> str|None: """ The file name for the back side of a :class:`Box`. Returns ------- str | None """ return self.asset.filename_back @filename_back.setter @blue.restrict def filename_back(self, filename_back: str|None) -> None: self.asset._prepare_for_modification(self) self.asset.filename_back = filename_back @property def builtin(self) -> str|None: """ If set, this attribute specifies a builtin procedural Texture. See examples above. Returns ------- str | None """ return self.asset.builtin @builtin.setter @blue.restrict def builtin(self, builtin: str|None) -> None: self.asset._prepare_for_modification(self) self.asset.builtin = builtin @property def color_1(self) -> blue.ColorType: """ The first color of a procedural Texture. Returns ------- blue.ColorType """ return self.asset.color_1 @color_1.setter @blue.restrict def color_1(self, color_1: object) -> None: self.asset._prepare_for_modification(self) self.asset.color_1 = color_1 @property def color_2(self) -> blue.ColorType: """ The second color of a procedural Texture. Returns ------- blue.ColorType """ return self.asset.color_2 @color_2.setter @blue.restrict def color_2(self, color_2: object) -> None: self.asset._prepare_for_modification(self) self.asset.color_2 = 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.asset.mark @mark.setter @blue.restrict def mark(self, mark: str|None) -> None: self.asset._prepare_for_modification(self) self.asset.mark = mark @property def color_mark(self) -> blue.ColorType: """ The mark color of a procedural Texture. Returns ------- blue.ColorType """ return self.asset.color_mark @color_mark.setter @blue.restrict def color_mark(self, color_mark: object) -> None: self.asset._prepare_for_modification(self) self.asset.color_mark = 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.asset.random @random.setter @blue.restrict def random(self, random: float) -> None: self.asset._prepare_for_modification(self) self.asset.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.asset.width @width.setter @blue.restrict def width(self, width: int) -> None: self.asset._prepare_for_modification(self) self.asset.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.asset.height @height.setter @blue.restrict def height(self, height: int) -> None: self.asset._prepare_for_modification(self) self.asset.height = height @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.asset.h_flip @h_flip.setter @blue.restrict def h_flip(self, h_flip: bool) -> None: self.asset._prepare_for_modification(self) self.asset.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.asset.v_flip @v_flip.setter @blue.restrict def v_flip(self, v_flip: bool) -> None: self.asset._prepare_for_modification(self) self.asset.v_flip = v_flip @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.asset.n_channel @n_channel.setter @blue.restrict def n_channel(self, n_channel: int) -> None: self.asset._prepare_for_modification(self) self.asset.n_channel = n_channel
[docs] class Plane(blue.PlaneTextureType, BaseTexture):
[docs] @blue.restrict def __init__(self, content: str|None = None, filename: 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 = 1000, height: int = 1000, h_flip: bool = False, v_flip: bool = False, n_channel: int = 3, name: str|None = None, asset: blue.TextureAssetType|None = None): if asset is None: self._asset = blue.assets.TextureAsset(content=content, file=filename, builtin=builtin, color_1=color_1, color_2=color_2, mark=mark, color_mark=color_mark, random=random, width=width, height=height, h_flip=h_flip, v_flip=v_flip, n_channel=n_channel, name=name) else: self._asset = asset self.asset._add(self) self.asset._TYPE = self._TYPE super().__init__(name=name)
[docs] class Box(blue.BoxTextureType, BaseTexture):
[docs] @blue.restrict def __init__(self, content: str|None = None, filename: str|None = None, grid_size: list[int] = [1, 1], grid_layout: str|None = None, filename_right: str|None = None, filename_left: str|None = None, filename_up: str|None = None, filename_down: str|None = None, filename_front: str|None = None, filename_back: 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 = 1000, height: int = 1000, h_flip: bool = False, v_flip: bool = False, n_channel: int = 3, name: str|None = None, asset: blue.TextureAssetType|None = None): if asset is None: self._asset = blue.assets.TextureAsset(content=content, file=filename, grid_size=grid_size, grid_layout=grid_layout, fileright=filename_right, fileleft=filename_left, fileup=filename_up, filedown=filename_down, filefront=filename_front, fileback=filename_back, builtin=builtin, color_1=color_1, color_2=color_2, mark=mark, color_mark=color_mark, random=random, width=width, height=height, h_flip=h_flip, v_flip=v_flip, n_channel=n_channel, name=name) else: self._asset = asset self.asset._add(self) self.asset._TYPE = self._TYPE super().__init__(name=name)
[docs] class Skybox(blue.SkyboxTextureType, Box):
[docs] @blue.restrict def __init__(self, content: str|None = None, filename: str|None = None, grid_size: list[int] = [1, 1], grid_layout: str|None = None, filename_right: str|None = None, filename_left: str|None = None, filename_up: str|None = None, filename_down: str|None = None, filename_front: str|None = None, filename_back: 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 = 1000, height: int = 1000, h_flip: bool = False, v_flip: bool = False, n_channel: int = 3, name: str|None = None, asset: blue.TextureAssetType|None = None): super().__init__(content=content, filename=filename, grid_size=grid_size, grid_layout=grid_layout, filename_right=filename_right, filename_left=filename_left, filename_up=filename_up, filename_down=filename_down, filename_front=filename_front, filename_back=filename_back, builtin=builtin, color_1=color_1, color_2=color_2, mark=mark, color_mark=color_mark, random=random, width=width, height=height, h_flip=h_flip, v_flip=v_flip, n_channel=n_channel, name=name, asset=asset)