import contextlib
import os
import pickle as pkl
from io import StringIO
import numpy as np
import pyvista as pv
import tetgen
import genesis as gs
import genesis.utils.mesh as mu
import genesis.utils.particle as pu
from genesis.ext import trimesh
from genesis.repr_base import RBC
[文档]class Mesh(RBC):
"""
Genesis's own triangle mesh object.
This is a wrapper of `trimesh.Trimesh` with some additional features and attributes. The internal trimesh object can be accessed via `self.trimesh`.
We perform both convexification and decimation to preprocess the mesh for simulation if specified.
Parameters
----------
surface : genesis.Surface
The mesh's surface object.
uvs : np.ndarray
The mesh's uv coordinates.
convexify : bool
Whether to convexify the mesh.
decimate : bool
Whether to decimate the mesh.
decimate_face_num : int
The target number of faces after decimation.
metadata : dict
The metadata of the mesh.
"""
def __init__(
self,
mesh,
surface=None,
uvs=None,
convexify=False,
decimate=False,
decimate_face_num=500,
metadata=dict(),
):
self._uid = gs.UID()
self._mesh = mesh
self._surface = surface
self._uvs = uvs
self._metadata = metadata
if self._surface.requires_uv(): # check uvs here
if self._uvs is None:
if "mesh_path" in metadata:
gs.logger.warning(
f"Texture given but asset missing uv info (or failed to load): {metadata['mesh_path']}"
)
else:
gs.logger.warning("Texture given but asset missing uv info (or failed to load).")
else:
self._uvs = None
if convexify:
self.convexify()
if decimate:
self.decimate(decimate_face_num, convexify)
[文档] def convexify(self):
"""
Convexify the mesh.
"""
if self._mesh.vertices.shape[0] > 3:
self._mesh = trimesh.convex.convex_hull(self._mesh)
self.clear_visuals()
[文档] def decimate(self, target_face_num, convexify):
"""
Decimate the mesh.
"""
if self._mesh.vertices.shape[0] > 3 and self._mesh.faces.shape[0] > target_face_num:
self._mesh = self._mesh.simplify_quadric_decimation(target_face_num)
# need to run convexify again after decimation, because sometimes decimating a convex-mesh can make it non-convex...
if convexify:
self.convexify()
self.clear_visuals()
[文档] def remesh(self, edge_len_abs=None, edge_len_ratio=0.01, fix=True):
"""
Remesh for tetrahedralization.
"""
rm_file_path = mu.get_remesh_path(self.verts, self.faces, edge_len_abs, edge_len_ratio, fix)
if not os.path.exists(rm_file_path):
gs.logger.info("Remeshing for tetrahedralization...")
with contextlib.redirect_stdout(StringIO()):
import pymeshlab
ms = pymeshlab.MeshSet()
ms.add_mesh(pymeshlab.Mesh(vertex_matrix=self.verts, face_matrix=self.faces))
if edge_len_abs is not None:
ms.meshing_isotropic_explicit_remeshing(targetlen=pymeshlab.PureValue(edge_len_abs))
else:
ms.meshing_isotropic_explicit_remeshing(targetlen=pymeshlab.PercentageValue(edge_len_ratio * 100))
m = ms.current_mesh()
verts, faces = m.vertex_matrix(), m.face_matrix()
# Maybe we need to fix the mesh in some extreme cases with open3d
# if fix:
# verts, faces = pymeshfix.clean_from_arrays(verts, faces)
os.makedirs(os.path.dirname(rm_file_path), exist_ok=True)
pkl.dump([verts, faces], open(rm_file_path, "wb"))
else:
gs.logger.debug("Remeshed file (`.rm`) found in cache.")
verts, faces = pkl.load(open(rm_file_path, "rb"))
self._mesh = trimesh.Trimesh(
vertices=verts,
faces=faces,
)
self.clear_visuals()
[文档] def tetrahedralize(self, order, mindihedral, minratio, nobisect, quality, verbose):
"""
Tetrahedralize the mesh.
"""
pv_obj = pv.PolyData(
self.verts, np.concatenate([np.full((self.faces.shape[0], 1), self.faces.shape[1]), self.faces], axis=1)
)
tet = tetgen.TetGen(pv_obj)
verts, elems = tet.tetrahedralize(
order=order, mindihedral=mindihedral, minratio=minratio, nobisect=nobisect, quality=quality, verbose=verbose
)
# visualize_tet(tet, pv_obj, show_surface=False, plot_cell_qual=False)
return verts, elems
[文档] def particlize(
self,
p_size=0.01,
sampler="random",
):
"""
Sample particles using the mesh volume.
"""
if "pbs" in sampler:
positions = pu.trimesh_to_particles_pbs(self._mesh, p_size, sampler)
if positions is None:
gs.logger.warning("`pbs` sampler failed. Falling back to `random` sampler.")
sampler = "random"
if sampler in ["random", "regular"]:
positions = pu.trimesh_to_particles_simple(self._mesh, p_size, sampler)
return positions
[文档] def clear_visuals(self):
"""
Clear the mesh's visual attributes by resetting the surface to gs.surfaces.Default().
"""
self._surface = gs.surfaces.Default()
self._surface.update_texture()
[文档] def get_unique_edges(self):
"""
Get the unique edges of the mesh.
"""
r_face = np.roll(self.faces, 1, axis=1)
edges = np.concatenate(np.array([self.faces, r_face]).T)
# do a first pass to remove duplicates
edges.sort(axis=1)
edges = np.unique(edges, axis=0)
edges = edges[edges[:, 0] != edges[:, 1]]
return edges
[文档] def copy(self):
"""
Copy the mesh.
"""
return Mesh(
mesh=self._mesh.copy(),
surface=self._surface.copy(),
uvs=self._uvs.copy() if self._uvs is not None else None,
metadata=self._metadata.copy(),
)
[文档] @classmethod
def from_trimesh(
cls, mesh, scale=None, convexify=False, decimate=False, decimate_face_num=500, metadata=dict(), surface=None
):
"""
Create a genesis.Mesh from a trimesh.Trimesh object.
"""
if surface is None:
surface = gs.surfaces.Default()
else:
surface = surface.copy()
mesh = mesh.copy()
try: # always parse uvs because roughness and normal map also need uvs
uvs = mesh.visual.uv.copy()
uvs[:, 1] = 1.0 - uvs[:, 1] # trimesh uses uvs starting from top left corner
except:
uvs = None
roughness_factor = None
color_image = None
color_factor = None
opacity = 1.0
if mesh.visual.defined:
if mesh.visual.kind == "texture":
material = mesh.visual.material
# TODO: Parsing PBR in obj or not
# trimesh from .obj file will never use PBR material, but that from .glb file will
if isinstance(material, trimesh.visual.material.PBRMaterial):
# color_image = None
# color_factor = None
if material.baseColorTexture is not None:
color_image = mu.PIL_to_array(material.baseColorTexture)
if material.baseColorFactor is not None:
color_factor = tuple(np.array(material.baseColorFactor, dtype=float) / 255.0)
if material.roughnessFactor is not None:
roughness_factor = (material.roughnessFactor,)
elif isinstance(material, trimesh.visual.material.SimpleMaterial):
if material.image is not None:
color_image = mu.PIL_to_array(material.image)
elif material.diffuse is not None:
color_factor = tuple(np.array(material.diffuse, dtype=float) / 255.0)
if material.glossiness is not None:
roughness_factor = ((2 / (material.glossiness + 2)) ** (1.0 / 4.0),)
opacity = float(material.kwargs.get("d", [1.0])[0])
if opacity < 1.0:
if color_factor is None:
color_factor = (1.0, 1.0, 1.0, opacity)
else:
color_factor = (*color_factor[:3], color_factor[3] * opacity)
else:
gs.raise_exception()
else:
# TODO: support vertex/face colors in luisa
color_factor = tuple(np.array(mesh.visual.main_color, dtype=float) / 255.0)
else:
# use white color as default
color_factor = (1.0, 1.0, 1.0, 1.0)
color_texture = mu.create_texture(color_image, color_factor, "srgb")
if color_texture is not None:
opacity_texture = color_texture.check_dim(3)
roughness_texture = mu.create_texture(None, roughness_factor, "linear")
surface.update_texture(
color_texture=color_texture,
opacity_texture=opacity_texture,
roughness_texture=roughness_texture,
)
mesh.visual = mu.surface_uvs_to_trimesh_visual(surface, uvs, len(mesh.vertices))
if scale is not None:
mesh.vertices *= scale
return cls(
mesh=mesh,
surface=surface,
uvs=uvs,
convexify=convexify,
decimate=decimate,
decimate_face_num=decimate_face_num,
metadata=metadata,
)
[文档] @classmethod
def from_attrs(cls, verts, faces, normals=None, surface=None, uvs=None, scale=None):
"""
Create a genesis.Mesh from mesh attribtues including vertices, faces, and normals.
"""
if surface is None:
surface = gs.surfaces.Default()
else:
surface = surface.copy()
return cls(
mesh=trimesh.Trimesh(
vertices=verts * scale if scale is not None else verts,
faces=faces,
vertex_normals=normals,
visual=mu.surface_uvs_to_trimesh_visual(surface, uvs, len(verts)),
process=False,
),
surface=surface,
uvs=uvs,
)
[文档] @classmethod
def from_morph_surface(cls, morph, surface=None):
"""
Create a genesis.Mesh from morph and surface options.
If the morph is a Mesh morph (morphs.Mesh), it could contain multiple submeshes, so we return a list.
"""
if isinstance(morph, gs.options.morphs.Mesh):
if morph.file.endswith(("obj", "ply", "stl")):
meshes = mu.parse_mesh_trimesh(morph.file, morph.group_by_material, morph.scale, surface)
elif morph.file.endswith(("glb", "gltf")):
if morph.parse_glb_with_trimesh:
meshes = mu.parse_mesh_trimesh(morph.file, morph.group_by_material, morph.scale, surface)
else:
meshes = mu.parse_mesh_glb(morph.file, morph.group_by_material, morph.scale, surface)
elif hasattr(morph, "files") and len(morph.files) > 0: # for meshset
meshes = morph.files
assert all([isinstance(v, trimesh.Trimesh) for v in meshes])
meshes = [mu.trimesh_to_mesh(v, morph.scale, surface) for v in meshes]
else:
gs.raise_exception(
f"File type not supported (yet). Submit a feature request if you need this: {morph.file}."
)
return meshes
else:
if isinstance(morph, gs.options.morphs.Box):
tmesh = mu.create_box(extents=morph.size)
elif isinstance(morph, gs.options.morphs.Cylinder):
tmesh = mu.create_cylinder(radius=morph.radius, height=morph.height)
elif isinstance(morph, gs.options.morphs.Sphere):
tmesh = mu.create_sphere(radius=morph.radius)
else:
gs.raise_exception()
return cls.from_trimesh(tmesh, surface=surface)
[文档] def set_color(self, color):
"""
Set the mesh's color.
"""
color_texture = gs.textures.ColorTexture(color=tuple(color))
opacity_texture = color_texture.check_dim(3)
self._surface.update_texture(color_texture=color_texture, opacity_texture=opacity_texture, force=True)
self.update_trimesh_visual()
[文档] def update_trimesh_visual(self):
"""
Update the trimesh obj's visual attributes using its surface and uvs.
"""
self._mesh.visual = mu.surface_uvs_to_trimesh_visual(self.surface, self.uvs, len(self.verts))
[文档] def show(self):
"""
Visualize the mesh using trimesh's built-in viewer.
"""
return self._mesh.show()
@property
def uid(self):
"""
Return the mesh's uid.
"""
return self._uid
@property
def trimesh(self):
"""
Return the mesh's trimesh object.
"""
return self._mesh
@property
def is_convex(self):
"""
Whether the mesh is convex.
"""
return self._mesh.is_convex
@property
def metadata(self):
"""
Metadata of the mesh.
"""
return self._metadata
@property
def verts(self):
"""
Vertices of the mesh.
"""
return self._mesh.vertices
@verts.setter
def verts(self, verts):
"""
Set the vertices of the mesh.
"""
assert len(verts) == len(self.verts)
self._mesh.vertices = verts
@property
def faces(self):
"""
Faces of the mesh.
"""
return self._mesh.faces
@property
def normals(self):
"""
Normals of the mesh.
"""
return self._mesh.vertex_normals
@property
def surface(self):
"""
Surface of the mesh.
"""
return self._surface
@property
def uvs(self):
"""
UVs of the mesh.
"""
return self._uvs
@property
def area(self):
"""
Surface area of the mesh.
"""
return self._mesh.area
@property
def volume(self):
"""
Volume of the mesh.
"""
return self._mesh.volume