Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
314965a3cThis file is pinned to the selected release. Download the original to open it in your design tool.
# Copyright 2024 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Built-in shapes for USD exporter."""
from typing import Any, Dict, Optional, Tuple, Union
import mujoco
import numpy as np
def get_triangle_uvs(
vertices: np.ndarray,
triangles: np.ndarray,
texture_type: Optional[mujoco.mjtTexture]
):
"""Returns UV coordinates for a given mesh."""
if texture_type is None:
return None
triangle_uvs = []
if texture_type == mujoco.mjtTexture.mjTEXTURE_2D:
triangle_uvs = [
[vertices[i][0], vertices[i][1]] for i in np.nditer(triangles)
]
elif texture_type == mujoco.mjtTexture.mjTEXTURE_CUBE:
center = np.mean(vertices, axis=0)
for vertex_id in np.nditer(triangles):
x, y, z = vertices[vertex_id] - center
abs_x, abs_y, abs_z = abs(x), abs(y), abs(z)
u = 0
v = 0
if x > 0 and abs_x >= abs_y and abs_x >= abs_z:
u = -z / abs_x
v = y / abs_x
elif x <= 0 and abs_x >= abs_y and abs_x >= abs_z:
u = z / abs_x
v = y / abs_x
elif y > 0 and abs_y >= abs_x and abs_y >= abs_z:
u = x / abs_y
v = -z / abs_y
elif y <= 0 and abs_y >= abs_x and abs_y >= abs_z:
u = x / abs_y
v = z / abs_y
elif z > 0 and abs_z >= abs_x and abs_z >= abs_y:
u = x / abs_z
v = y / abs_z
elif z <= 0 and abs_z >= abs_x and abs_z >= abs_y:
u = -x / abs_z
v = y / abs_z
u = (u + 1.0) / 2.0
v = (v + 1.0) / 2.0
v /= 6
assert 0 <= u and u <= 1 and 0 <= v and v <= 1
triangle_uvs.append([u, v])
elif texture_type == mujoco.mjtTexture.mjTEXTURE_SKYBOX:
# defaults to 2D mapping temporarily
triangle_uvs = [
[vertices[i][0], vertices[i][1]] for i in np.nditer(triangles)
]
return np.array(triangle_uvs)
class TriangleMesh:
"""Store UV and geometry information for a primitive mesh.
Attributes:
vertices: A numpy array of vertices.
triangles: A numpy array of triangles.
triangle_uvs: A numpy array of UV coordinates.
"""
def __init__(self,
vertices: np.ndarray,
triangles: np.ndarray,
triangle_uvs: np.ndarray):
"""Creates a TriangleMesh object.
Args:
vertices: A numpy array of vertices.
triangles: A numpy array of triangles.
triangle_uvs: A numpy array of UV coordinates.
"""
self.vertices = vertices
self.triangles = triangles
self.triangle_uvs = triangle_uvs
@classmethod
def create_box(
cls,
width: float,
height: float,
depth: float,
texture_type: Optional[mujoco.mjtTexture]
) -> "TriangleMesh":
"""Creates a box."""
vertices = np.array([[0.0, 0.0, 0.0],
[width, 0.0, 0.0],
[0.0, 0.0, depth],
[width, 0.0, depth],
[0.0, height, 0.0],
[width, height, 0.0],
[0.0, height, depth],
[width, height, depth]])
triangles = np.array([[4, 7, 5],
[4, 6, 7],
[0, 2, 4],
[2, 6, 4],
[0, 1, 2],
[1, 3, 2],
[1, 5, 7],
[1, 7, 3],
[2, 3, 7],
[2, 7, 6],
[0, 4, 1],
[1, 4, 5]])
triangle_uvs = get_triangle_uvs(vertices, triangles, texture_type)
return TriangleMesh(vertices, triangles, triangle_uvs)
@classmethod
def create_sphere(
cls,
radius: float,
texture_type: Optional[mujoco.mjtTexture],
resolution: int
) -> "TriangleMesh":
"""Creates a sphere."""
vertices = []
triangles = []
for i in range(2*resolution + 1):
phi = np.pi * i / (2*resolution)
for j in range(resolution + 1):
theta = 2 * np.pi * j / resolution
x = radius * np.sin(phi) * np.cos(theta)
y = radius * np.sin(phi) * np.sin(theta)
z = radius * np.cos(phi)
vertices.append([x, y, z])
for i in range(2*resolution):
for j in range(resolution):
first = i * (resolution + 1) + j
second = first + resolution + 1
triangles.append([first, second, first + 1])
triangles.append([second, second + 1, first + 1])
vertices = np.array(vertices)
triangles = np.array(triangles)
triangle_uvs = get_triangle_uvs(vertices, triangles, texture_type)
return TriangleMesh(vertices, triangles, triangle_uvs)
@classmethod
def create_hemisphere(
cls,
radius: float,
texture_type: Optional[mujoco.mjtTexture],
resolution: int,
) -> "TriangleMesh":
"""Creates a hemisphere."""
vertices = []
triangles = []
for i in range(resolution + 1):
phi = np.pi / 2 * i / (resolution)
for j in range(resolution + 1):
theta = 2 * np.pi * j / resolution
x = radius * np.sin(phi) * np.cos(theta)
y = radius * np.sin(phi) * np.sin(theta)
z = radius * np.cos(phi)
vertices.append([x, y, z])
vertices.append([0, 0, 0])
for i in range(resolution):
for j in range(resolution):
first = i * (resolution + 1) + j
second = first + resolution + 1
triangles.append([first, second, first + 1])
triangles.append([second, second + 1, first + 1])
for i in range(resolution):
first = resolution * (resolution + 1) + i
triangles.append([first, first + 1, len(vertices) - 1])
vertices = np.array(vertices)
triangles = np.array(triangles)
triangle_uvs = get_triangle_uvs(vertices, triangles, texture_type)
return TriangleMesh(vertices, triangles, triangle_uvs)
@classmethod
def create_cylinder(
cls,
radius: float,
height: float,
texture_type: Optional[mujoco.mjtTexture],
resolution: int
) -> "TriangleMesh":
"""Creates a cylinder."""
vertices = []
triangles = []
# adding all the vertices for the cylinder including
# two center vertices at ends
for i in range(2):
z = 0 if i == 0 else height
for j in range(resolution + 1):
theta = 2 * np.pi * j / resolution
x = radius * np.cos(theta)
y = radius * np.sin(theta)
vertices.append([x, y, z])
vertices.append([0, 0, 0])
vertices.append([0, 0, height])
# constructing the end faces for the cylinder
for i in range(2):
for j in range(resolution):
first = (resolution + 1) * i + j
triangles.append([first, first + 1, len(vertices) - (2 - i)])
# constructing side of cylinder
for i in range(resolution):
second = resolution + 1 + i
triangles.append([i, second, second + 1])
triangles.append([i, i + 1, second + 1])
vertices = np.array(vertices)
triangles = np.array(triangles)
triangle_uvs = get_triangle_uvs(vertices, triangles, texture_type)
return TriangleMesh(vertices, triangles, triangle_uvs)
def translate(self, translation: np.ndarray) -> None:
self.vertices = self.vertices + translation
def rotate(self, rotation: np.ndarray, center: Tuple[float, ...]) -> None:
translated_point = self.vertices - center
self.vertices = np.dot(translated_point, rotation) + center
def scale(self, scale: np.ndarray) -> None:
self.vertices = self.vertices * scale
def get_center(self):
center = np.mean(self.vertices, axis=0)
return center
def __add__(self, other):
if isinstance(other, TriangleMesh):
new_vertices = np.vstack((self.vertices, other.vertices))
other_triangles = other.triangles + len(self.vertices)
new_triangles = np.vstack((self.triangles, other_triangles))
new_triangle_uvs = None
if self.triangle_uvs is not None:
new_triangle_uvs = np.vstack((self.triangle_uvs, other.triangle_uvs))
return TriangleMesh(new_vertices, new_triangles, new_triangle_uvs)
raise TypeError(f"Cannot add TriangleMesh with {type(other)}")
def decouple_config(config: Dict[str, Any]):
"""Breaks a shape config into is subcomponent shapes."""
decoupled_config = []
for key, value in config.items():
if key == "name":
continue
decoupled_config.append({
"parent_name": config["name"],
"name": config["name"] + "_" + key,
key: value.copy(),
})
return decoupled_config
def mesh_config_generator(
name: str,
geom_type: Union[int, mujoco.mjtGeom],
size: np.ndarray,
decouple: bool = False,
):
"""Creates a config for a particular mesh."""
if geom_type == mujoco.mjtGeom.mjGEOM_PLANE:
config = {
"name": name,
"box": {
"width": size[0] * 2 if size[0] > 0 else 100,
"height": size[1] * 2 if size[1] > 0 else 100,
"depth": 0.001,
"map_texture_to_each_face": True,
},
}
elif geom_type == mujoco.mjtGeom.mjGEOM_SPHERE:
config = {"name": name, "sphere": {"radius": float(size[0])}}
elif geom_type == mujoco.mjtGeom.mjGEOM_CAPSULE:
cylinder = mesh_config_generator(name, mujoco.mjtGeom.mjGEOM_CYLINDER, size)
cylinder["cylinder"]["transform"] = {
"transform": {"translate": (0, 0, size[2])}
}
config = {
"name": name,
"cylinder": cylinder["cylinder"],
"left_hemisphere": {
"radius": size[0],
"transform": {
"rotate": (np.pi, 0, 0),
},
},
"right_hemisphere": {
"radius": size[0],
"transform": {"translate": (0, 0, 2*size[2])},
}
}
elif geom_type == mujoco.mjtGeom.mjGEOM_ELLIPSOID:
sphere = mesh_config_generator(
name, mujoco.mjtGeom.mjGEOM_SPHERE, np.array([1.0])
)
sphere["sphere"]["transform"] = {"scale": tuple(size)}
config = {
"name": name,
"sphere": sphere["sphere"],
}
elif geom_type == mujoco.mjtGeom.mjGEOM_CYLINDER:
config = {
"name": name,
"cylinder": {
"radius": size[0],
"height": size[2] * 2,
},
}
elif geom_type == mujoco.mjtGeom.mjGEOM_BOX:
config = {
"name": name,
"box": {
"width": size[0] * 2,
"height": size[1] * 2,
"depth": size[2] * 2,
},
}
else:
raise NotImplementedError(
f"{geom_type} primitive geom type not implemented with USD integration"
)
if decouple:
config = decouple_config(config)
return config
def mesh_factory(
mesh_config: Dict[str, Any],
texture_type: Optional[mujoco.mjtTexture],
resolution: int = 100,
):
"""Generates a mesh given a config consisting of shapes."""
assert "name" in mesh_config
mesh = None
for shape, config in mesh_config.items():
if "name" in shape:
continue
if "box" in shape:
prim_mesh = TriangleMesh.create_box(
width=mesh_config[shape]["width"],
height=mesh_config[shape]["height"],
depth=mesh_config[shape]["depth"],
texture_type=texture_type
)
elif "hemisphere" in shape:
prim_mesh = TriangleMesh.create_hemisphere(
radius=mesh_config[shape]["radius"],
texture_type=texture_type,
resolution=resolution
)
elif "sphere" in shape:
prim_mesh = TriangleMesh.create_sphere(
radius=mesh_config[shape]["radius"],
texture_type=texture_type,
resolution=resolution
)
elif "cylinder" in shape:
prim_mesh = TriangleMesh.create_cylinder(
radius=mesh_config[shape]["radius"],
height=mesh_config[shape]["height"],
texture_type=texture_type,
resolution=resolution
)
else:
raise ValueError("Shape not supported")
if "transform" in config:
if "rotate" in config["transform"]:
rotation = np.zeros(9)
quat = np.zeros(4)
euler = config["transform"]["rotate"]
seq = "xyz"
mujoco.mju_euler2Quat(quat, euler, seq)
mujoco.mju_quat2Mat(rotation, quat)
rotation = rotation.reshape((3, 3))
prim_mesh.rotate(rotation, center=(0, 0, 0))
if "scale" in config["transform"]:
prim_mesh.scale(config["transform"]["scale"])
if "translate" in config["transform"]:
prim_mesh.translate(config["transform"]["translate"])
if not mesh:
mesh = prim_mesh
else:
mesh += prim_mesh
if mesh is None:
raise ValueError("Mesh not found.")
return mesh_config["name"], mesh