Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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 @@ -314,7 +314,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
146 changes: 51 additions & 95 deletions genesis/engine/entities/fem_entity.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from functools import wraps
from pathlib import Path
from typing import NamedTuple

import igl
import numpy as np
import quadrants as qd
import torch
import trimesh

import genesis as gs
import genesis.utils.element as eu
Expand All @@ -19,6 +21,17 @@
from .base_entity import Entity


class FEMVisGeom(NamedTuple):
"""A visual geom of a FEM entity: a render mesh whose vertices track a subset of the simulated vertices.

'sim_verts_idx' maps each render-mesh vertex to the simulated vertex standing for it; vertices co-located
across visual geoms or duplicated by texture seams share a single simulated vertex.
"""

vmesh: gs.Mesh
sim_verts_idx: np.ndarray


def assert_muscle(method):
@wraps(method)
def wrapper(self, *args, **kwargs):
Expand Down Expand Up @@ -69,9 +82,6 @@ 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.sample()

# Check if this is cloth (elements are already triangles)
Expand Down Expand Up @@ -359,7 +369,8 @@ def instantiate(self, verts, elems):
Array of vertex positions with shape (n_vertices, 3).

elems : np.ndarray
Array of tetrahedral elements with shape (n_elements, 4), indexing into verts.
Array of elements indexing into verts: tetrahedra with shape (n_elements, 4), or surface triangles with
shape (n_elements, 3) for Cloth material.

Raises
------
Expand Down Expand Up @@ -391,72 +402,41 @@ def instantiate(self, verts, elems):

def sample(self):
"""
Sample mesh and elements based on the entity's morph type.

For Cloth material, loads surface mesh directly without tetrahedralization.
For regular FEM materials, tetrahedralizes the mesh.
Build the entity's visual geoms and simulation mesh from its morph.

Raises
------
Exception
If the morph type is unsupported.
Each morph sub-mesh becomes a visual geom with its own surface and UVs, while the simulation operates on a
single welded copy of their vertices, tracked through 'FEMVisGeom.sim_verts_idx': welding and
tetrahedralization both keep the input vertices first and in order, so these maps remain valid indices into
the simulated vertices.
"""
from genesis.engine.materials.FEM.cloth import Cloth as ClothMaterial

is_cloth = isinstance(self.material, ClothMaterial)
self._uvs = None
meshes = gs.Mesh.from_morph_surface(self._morph, self._surface)
surface_verts, surface_faces, sim_verts_maps = mu.merge_submeshes(
[mesh.verts for mesh in meshes], [mesh.faces for mesh in meshes]
)
self._vgeoms = gs.List(
FEMVisGeom(vmesh=mesh, sim_verts_idx=verts_idx) for mesh, verts_idx in zip(meshes, sim_verts_maps)
)

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}.")
if isinstance(self.material, ClothMaterial):
# Cloth needs no tetrahedralization: the welded surface triangles are the simulation elements.
verts = surface_verts + self._morph.pos
elems = surface_faces
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}.")

self.instantiate(*eu.split_all_surface_tets(verts, elems))
# Tetgen refinement depends on the absolute coordinates of its input. File meshes are tetrahedralized
# untranslated so the result, and its on-disk cache, are shared across all placements of the same asset;
# primitives keep the position baked in, as the simulated rest state is sensitive to the exact refinement.
is_mesh_morph = isinstance(self._morph, gs.options.morphs.Mesh)
if not is_mesh_morph:
surface_verts = surface_verts + self._morph.pos
surface_trimesh = trimesh.Trimesh(vertices=surface_verts, faces=surface_faces, process=False)
verts, elems = eu.mesh_to_elements(surface_trimesh, tet_cfg=self.tet_cfg)
if is_mesh_morph:
verts = verts + self._morph.pos
verts, elems = eu.split_all_surface_tets(verts, elems)

self.instantiate(verts, elems)

def _add_to_solver(self, in_backward=False):
from genesis.engine.materials.FEM.cloth import Cloth as ClothMaterial
Expand All @@ -471,24 +451,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(
self._solver._kernel_add_cloth(
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
elems_np = self.elems.astype(gs.np_int, copy=False)
self._solver._kernel_add_elements(
f=self._sim.cur_substep_local,
Expand All @@ -505,7 +477,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 @@ -1077,9 +1048,14 @@ def n_vertices(self):
"""Number of vertices in the FEM entity."""
return len(self.init_positions)

@property
def vgeoms(self):
"""The list of visual geoms (`FEMVisGeom`) in the entity, one per morph sub-mesh."""
return self._vgeoms

@property
def n_elements(self):
"""Number of tetrahedral elements in the FEM entity."""
"""Number of simulation elements: surface triangles for Cloth material, tetrahedra otherwise."""
return len(self.elems)

@property
Expand All @@ -1102,21 +1078,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 @@ -1127,11 +1088,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
10 changes: 4 additions & 6 deletions genesis/engine/entities/particle_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,10 @@ def __init__(
self._surface = self._vmesh[0].surface

elif isinstance(self._morph, (gs.options.morphs.Primitive, gs.options.morphs.Mesh)):
self._vmesh = gs.Mesh.from_morph_surface(self.morph, self.surface)
if isinstance(self._vmesh, list):
if len(self._vmesh) > 1:
gs.raise_exception("Mesh file with multiple sub-meshes are not supported.")
else:
self._vmesh = self._vmesh[0]
meshes = gs.Mesh.from_morph_surface(self.morph, self.surface)
if len(meshes) > 1:
gs.raise_exception("Mesh file with multiple sub-meshes are not supported.")
self._vmesh = meshes[0]
self._surface = self._vmesh.surface

else:
Expand Down
11 changes: 7 additions & 4 deletions genesis/engine/materials/FEM/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,18 +89,21 @@ def _build_noop(self, fem_solver):
def _pre_compute_noop(self, J, F, i_e, i_b):
pass

# The noop dispatch funcs below return a zero material response: they are compiled into the solver kernels for
# materials whose physics lives outside the FEM solver (e.g. Cloth is simulated by the IPC coupler), and
# quadrants funcs cannot raise.
@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)
Comment on lines 96 to +97

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard cloth no-op stress outside IPC

When a FEM.Cloth entity is stepped in the default Scene configuration, the coupler is LegacyCouplerOptions, so FEMSolver.substep_pre_coupling() still runs compute_vel() over self.n_elements. Cloth is added only through _kernel_add_cloth(), which does not initialize elements_i.mass_scaled/tet data, and returning a zero stress here lets compute_vel() continue to dv = ... / mass_scaled with the default zero mass, producing NaNs instead of a clear unsupported-configuration failure. This only affects Cloth or other no-op FEM materials outside IPCCoupler; reject that setup before stepping or initialize/skip those elements rather than registering a zero-stress FEM material.

Useful? React with 👍 / 👎.


@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
9 changes: 5 additions & 4 deletions genesis/engine/mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,11 +512,12 @@ def from_attrs(
)

@classmethod
def from_morph_surface(cls, morph, surface=None) -> "list[gs.Mesh] | gs.Mesh":
def from_morph_surface(cls, morph, surface=None) -> "list[gs.Mesh]":
"""
Create a genesis.Mesh from morph and surface options.
Create genesis.Mesh objects from morph and surface options.

If the morph is a Mesh morph (morphs.Mesh), it could contain multiple sub-meshes, so we return a list.
A list is always returned: a Mesh morph (morphs.Mesh) may contain multiple sub-meshes, while primitive
morphs yield a single mesh.
"""
if isinstance(morph, gs.options.morphs.Mesh):
if morph.is_format(gs.options.morphs.MESH_FORMATS):
Expand Down Expand Up @@ -545,7 +546,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 @@ -592,8 +592,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