Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added genesis/assets/meshes/Trashbag_rope.glb
Binary file not shown.
2 changes: 1 addition & 1 deletion genesis/engine/couplers/sap_coupler.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ def _init_hydroelastic_rigid_fields_and_info(self):
gs.raise_exception("Primitive plane not supported as user-specified collision geometries.")
volume = geom.get_trimesh().volume
tet_cfg = {"nobisect": False, "maxvolume": volume / 100}
mesh_verts, mesh_elems, _uvs = eu.mesh_to_elements(file=geom.get_trimesh(), tet_cfg=tet_cfg)
mesh_verts, mesh_elems = eu.mesh_to_elements(geom.get_trimesh(), tet_cfg=tet_cfg)
verts, elems = eu.split_all_surface_tets(mesh_verts, mesh_elems)
rigid_volume_verts.append(verts)
rigid_volume_elems.append(elems + offset)
Expand Down
167 changes: 79 additions & 88 deletions genesis/engine/entities/fem_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import numpy as np
import quadrants as qd
import torch
import trimesh

import genesis as gs
import genesis.utils.element as eu
Expand Down Expand Up @@ -69,9 +70,8 @@ def __init__(
self._el_start = el_start # offset for element index
self._s_start = s_start # offset for surface triangles
self._step_global_added = None

self._surface.update_texture()

self._render_meshes = []
self._sim_vert_maps = None
self.sample()

# Check if this is cloth (elements are already triangles)
Expand Down Expand Up @@ -369,10 +369,12 @@ def instantiate(self, verts, elems):
verts = verts.astype(gs.np_float, copy=False)
elems = elems.astype(gs.np_int, copy=False)

# rotate
# rotate and translate
R = gu.quat_to_R(np.array(self.morph.quat, dtype=gs.np_float))
verts_COM = verts.mean(axis=0)
init_positions = (verts - verts_COM) @ R.T + verts_COM
p = np.array(self.morph.pos, dtype=gs.np_float)
verts_translated = verts + p
verts_COM = verts_translated.mean(axis=0)
init_positions = (verts_translated - verts_COM) @ R.T + verts_COM

if not init_positions.shape[0] > 0:
gs.raise_exception("Entity has zero vertices.")
Expand All @@ -397,60 +399,68 @@ def sample(self):
from genesis.engine.materials.FEM.cloth import Cloth as ClothMaterial

is_cloth = isinstance(self.material, ClothMaterial)
self._uvs = None

if is_cloth:
# Cloth: load surface mesh directly (no tetrahedralization)
if isinstance(self.morph, gs.options.morphs.Mesh):
import trimesh

mesh = trimesh.load_mesh(self._morph.file)
verts = mesh.vertices * self._morph.scale + np.array(self._morph.pos)
faces = mesh.faces
# For cloth, we store faces as "elements" (treating them as surface elements)
self.instantiate(verts, faces)

# Load UVs from mesh (1:1 mapping for cloth).
# UVs are not always available in 3D file, in case they are missing we set the entity UVs to None when UVs are None,
# the solver will use 0 UVs for rendering. A mesh with 0 UVs means that no tangent directions can be recomputed,
# thus texture mapping and anisotropic surfaces will not work properly.
self._uvs = None
if isinstance(mesh.visual, trimesh.visual.texture.TextureVisuals) and mesh.visual.uv is not None:
self._uvs = mesh.visual.uv.astype(gs.np_float, copy=False)
else:
gs.raise_exception(f"Cloth material only supports Mesh morph. Got: {self.morph}.")
else:
# Regular FEM: tetrahedralize mesh
if isinstance(self.morph, gs.options.morphs.Sphere):
verts, elems = eu.sphere_to_elements(
pos=self._morph.pos,
radius=self._morph.radius,
tet_cfg=self.tet_cfg,
)
elif isinstance(self.morph, gs.options.morphs.Box):
verts, elems = eu.box_to_elements(
pos=self._morph.pos,
size=self._morph.size,
tet_cfg=self.tet_cfg,
)
elif isinstance(self.morph, gs.options.morphs.Cylinder):
verts, elems = eu.cylinder_to_elements()
elif isinstance(self.morph, gs.options.morphs.Mesh):
# We don't need to proces UVs here because the tetrahedralization process append new vertices
# and faces at the end of the vertex list, thus the original UVs are preserved at the beginning.
# We can't generate UVs for newly created internal vertices as it doesn't make sense but they're
# not used for rendering so it's fine.
verts, elems, self._uvs = eu.mesh_to_elements(
file=self._morph.file,
pos=self._morph.pos,
scale=self._morph.scale,
tet_cfg=self.tet_cfg,
)
else:
gs.raise_exception(f"Unsupported morph: {self.morph}.")
meshes = gs.Mesh.from_morph_surface(self._morph, self._surface)
self._render_meshes = list(meshes)

mesh_verts = [mesh.verts for mesh in meshes]
mesh_faces = [mesh.faces for mesh in meshes]
verts, faces = self._merge_elements(mesh_verts, mesh_faces)
self.instantiate(verts, faces)

else:
meshes = gs.Mesh.from_morph_surface(self._morph, self._surface)
self._render_meshes = list(meshes)

mesh_verts = [mesh.verts for mesh in meshes]
mesh_faces = [mesh.faces for mesh in meshes]
surface_verts, surface_faces = self._merge_elements(mesh_verts, mesh_faces)
surface_trimesh = trimesh.Trimesh(vertices=surface_verts, faces=surface_faces, process=False)
verts, elems = eu.mesh_to_elements(mesh=surface_trimesh, tet_cfg=self.tet_cfg)
self.instantiate(*eu.split_all_surface_tets(verts, elems))

def _merge_elements(self, mesh_verts_list, mesh_elems_list):
"""Merge multiple sub-meshes' vertices and elements, deduplicating shared vertices.

Concatenates all vertices, deduplicates by position, remaps element
indices, and builds ``_sim_vert_maps`` mapping each sub-mesh's local
vertex indices to the deduplicated sim vertex array.

Returns (combined_verts, combined_elems).
"""
all_verts_list = []
all_elems_list = []
sub_mesh_ranges = []
offset = 0
for verts, elems in zip(mesh_verts_list, mesh_elems_list):
v = np.asarray(verts, dtype=np.float64)
e = np.asarray(elems, dtype=np.int32)
all_verts_list.append(v)
all_elems_list.append(e + offset)
sub_mesh_ranges.append((offset, len(v)))
offset += len(v)
all_verts = np.vstack(all_verts_list)
all_elems = np.vstack(all_elems_list)

# Deduplicate by quantizing positions
quantized = np.round(all_verts * 1e8).astype(np.int64)
_, unique_idx, remap = np.unique(quantized, axis=0, return_index=True, return_inverse=True)
sorted_order = np.argsort(unique_idx)
rank = np.empty_like(sorted_order)
rank[sorted_order] = np.arange(len(sorted_order))
global_remap = rank[remap]

verts = all_verts[np.sort(unique_idx)].astype(gs.np_float)
elems = global_remap[all_elems].astype(gs.np_int)

# Build per-sub-mesh sim_vert_maps
self._sim_vert_maps = []
for sub_offset, sub_n_verts in sub_mesh_ranges:
self._sim_vert_maps.append(global_remap[sub_offset : sub_offset + sub_n_verts].astype(gs.np_int))

return verts, elems

def _add_to_solver(self, in_backward=False):
from genesis.engine.materials.FEM.cloth import Cloth as ClothMaterial

Expand All @@ -464,24 +474,16 @@ def _add_to_solver(self, in_backward=False):

# Convert to appropriate numpy array types
verts_numpy = tensor_to_array(self.init_positions, dtype=gs.np_float)
uvs_np = self._uvs if self._uvs is not None else np.zeros((0, 2), dtype=gs.np_float)

if is_cloth:
# Cloth: add only vertices and surfaces for rendering (no physics computation)
gs.logger.info(
f"Entity {self.uid} is cloth - adding to FEM solver for rendering only (physics managed by IPC)"
)
self._solver._kernel_add_cloth_for_rendering(
gs.logger.info(f"Entity {self.uid} is cloth - adding to FEM solver for position tracking only")
self._solver._kernel_add_elements_render(
f=self._sim.cur_substep_local,
n_surfaces=self._n_surfaces,
v_start=self._v_start,
s_start=self._s_start,
verts=verts_numpy,
tri2v=self._surface_tri_np,
uvs=uvs_np,
)
else:
# Regular FEM: add vertices, elements, and surfaces for physics and rendering
# Regular FEM: add vertices, elements, and surfaces for physics
elems_np = self.elems.astype(gs.np_int, copy=False)
self._solver._kernel_add_elements(
f=self._sim.cur_substep_local,
Expand All @@ -498,7 +500,6 @@ def _add_to_solver(self, in_backward=False):
elems=elems_np,
tri2v=self._surface_tri_np,
tri2el=self._surface_el_np,
uvs=uvs_np,
)

self.active = True
Expand Down Expand Up @@ -1070,9 +1071,19 @@ def n_vertices(self):
"""Number of vertices in the FEM entity."""
return len(self.init_positions)

@property
def render_meshes(self):
"""Per-sub-mesh render meshes, decoupled from the sim mesh topology."""
return self._render_meshes

@property
def sim_vert_maps(self):
"""Per-sub-mesh maps from render-vertex index to sim-vertex index."""
return self._sim_vert_maps

@property
def n_elements(self):
"""Number of tetrahedral elements in the FEM entity."""
"""Number of elements (triangles for cloth, tets for volumetric)."""
return len(self.elems)

@property
Expand All @@ -1095,21 +1106,6 @@ def s_start(self):
"""Global surface triangle index offset for this entity."""
return self._s_start

@property
def morph(self):
"""Morph specification used to generate the FEM mesh."""
return self._morph

@property
def material(self):
"""Material properties of the FEM entity."""
return self._material

@property
def surface(self):
"""Surface for rendering."""
return self._surface

@property
def n_surface_vertices(self):
"""Number of unique vertices involved in surface triangles."""
Expand All @@ -1120,11 +1116,6 @@ def surface_triangles(self):
"""Surface triangles of the FEM mesh."""
return self._surface_tri_np

@property
def uvs(self):
"""UV coordinates for this entity's vertices, or None if not available."""
return self._uvs

@property
def tet_cfg(self):
"""Configuration of tetrahedralization."""
Expand Down
8 changes: 4 additions & 4 deletions genesis/engine/materials/FEM/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,16 +91,16 @@ def _pre_compute_noop(self, J, F, i_e, i_b):

@qd.func
def _update_stress_noop(self, mu, lam, J, F, actu, m_dir):
raise NotImplementedError
return qd.Matrix.zero(gs.qd_float, 3, 3)

@qd.func
def _compute_energy_gradient_hessian_noop(self, mu, lam, J, F, actu, m_dir, i_e, i_b, hessian_field):
raise NotImplementedError
pass

@qd.func
def _compute_energy_gradient_noop(self, mu, lam, J, F, actu, m_dir, i_e, i_b):
raise NotImplementedError
pass

@qd.func
def _compute_energy_noop(self, mu, lam, J, F, actu, m_dir, i_e, i_b):
raise NotImplementedError
pass
2 changes: 1 addition & 1 deletion genesis/engine/mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ def from_morph_surface(cls, morph, surface=None) -> "list[gs.Mesh] | gs.Mesh":
else:
gs.raise_exception(f"Morph {morph} not supported by this method.")

return cls.from_trimesh(tmesh, surface=surface)
return [cls.from_trimesh(tmesh, surface=surface)]

def set_color(self, color):
"""
Expand Down
7 changes: 5 additions & 2 deletions genesis/engine/scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -582,8 +582,11 @@ def add_mesh_light(

if not isinstance(morph, (gs.morphs.Primitive, gs.morphs.Mesh)):
gs.raise_exception("Light morph only supports `gs.morphs.Primitive` or `gs.morphs.Mesh`.")
mesh = gs.Mesh.from_morph_surface(morph, gs.surfaces.Plastic(smooth=False))
self._visualizer.add_mesh_light(mesh, color, intensity, morph.pos, morph.quat, revert_dir, double_sided, cutoff)
meshes = gs.Mesh.from_morph_surface(morph, gs.surfaces.Plastic(smooth=False))
for mesh in meshes:
self._visualizer.add_mesh_light(
mesh, color, intensity, morph.pos, morph.quat, revert_dir, double_sided, cutoff
)

@gs.assert_unbuilt
def add_light(
Expand Down
Loading
Loading