Skip to content

Commit 1c62c0c

Browse files
committed
Add UVs to FEM solver
1 parent 6365934 commit 1c62c0c

File tree

4 files changed

+70
-11
lines changed

4 files changed

+70
-11
lines changed

genesis/engine/entities/fem_entity.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@ def sample(self):
389389
from genesis.engine.materials.FEM.cloth import Cloth as ClothMaterial
390390

391391
is_cloth = isinstance(self.material, ClothMaterial)
392+
self._uvs = None # Initialize UVs storage (indexed same as FEM vertices)
392393

393394
if is_cloth:
394395
# Cloth: load surface mesh directly (no tetrahedralization)
@@ -400,6 +401,14 @@ def sample(self):
400401
faces = mesh.faces
401402
# For cloth, we store faces as "elements" (treating them as surface elements)
402403
self.instantiate(verts, faces)
404+
405+
# Load UVs from mesh (1:1 mapping for cloth)
406+
try:
407+
uvs = mesh.visual.uv.copy()
408+
uvs[:, 1] = 1.0 - uvs[:, 1] # flip V coordinate (trimesh uses top-left origin)
409+
self._uvs = uvs.astype(gs.np_float)
410+
except AttributeError:
411+
self._uvs = None
403412
else:
404413
gs.raise_exception(f"Cloth material only supports Mesh morph. Got: {self.morph}.")
405414
else:
@@ -419,11 +428,12 @@ def sample(self):
419428
elif isinstance(self.morph, gs.options.morphs.Cylinder):
420429
verts, elems = eu.cylinder_to_elements()
421430
elif isinstance(self.morph, gs.options.morphs.Mesh):
422-
verts, elems = eu.mesh_to_elements(
431+
verts, elems, self._uvs = eu.mesh_to_elements(
423432
file=self._morph.file,
424433
pos=self._morph.pos,
425434
scale=self._morph.scale,
426435
tet_cfg=self.tet_cfg,
436+
return_uvs=True,
427437
)
428438
else:
429439
gs.raise_exception(f"Unsupported morph: {self.morph}.")
@@ -477,8 +487,23 @@ def _add_to_solver(self, in_backward=False):
477487
tri2el=self._surface_el_np,
478488
)
479489

490+
# Add UVs to solver if available
491+
self._add_uvs_to_solver()
492+
480493
self.active = True
481494

495+
def _add_uvs_to_solver(self):
496+
"""
497+
Copy UV coordinates to the solver's surface_render_uvs field.
498+
"""
499+
if self._uvs is None:
500+
return
501+
502+
# Copy UVs to solver field starting at v_start
503+
uvs_np = self._uvs.astype(gs.np_float, copy=False)
504+
for i, uv in enumerate(uvs_np):
505+
self._solver.surface_render_uvs[self._v_start + i] = uv
506+
482507
def compute_pressure_field(self):
483508
"""
484509
Compute the pressure field for the FEM entity based on its tetrahedral elements.
@@ -1074,6 +1099,11 @@ def surface_triangles(self):
10741099
"""Surface triangles of the FEM mesh."""
10751100
return self._surface_tri_np
10761101

1102+
@property
1103+
def uvs(self):
1104+
"""UV coordinates for this entity's vertices, or None if not available."""
1105+
return self._uvs
1106+
10771107
@property
10781108
def tet_cfg(self):
10791109
"""Configuration of tetrahedralization."""

genesis/engine/solvers/fem_solver.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,13 @@ def init_surface_fields(self):
240240
layout=ti.Layout.SOA,
241241
)
242242

243+
# UV coordinates for rendering (per-vertex UVs, initialized to zeros)
244+
self.surface_render_uvs = ti.field(
245+
dtype=gs.ti_vec2,
246+
shape=(max(n_vertices_max, 1),),
247+
needs_grad=False,
248+
)
249+
243250
def _init_surface_info(self):
244251
self.vertices_on_surface = ti.field(dtype=gs.ti_bool, shape=(self.n_vertices,))
245252
self.elements_on_surface = ti.field(dtype=gs.ti_bool, shape=(self.n_elements,))
@@ -1096,8 +1103,9 @@ def get_state_render(self, f):
10961103
self.get_state_render_kernel(f)
10971104
vertices = self.surface_render_v.vertices
10981105
indices = self.surface_render_f.indices
1106+
uvs = self.surface_render_uvs
10991107

1100-
return vertices, indices
1108+
return vertices, indices, uvs
11011109

11021110
def get_forces(self):
11031111
"""
@@ -1378,7 +1386,8 @@ def get_state_render_kernel(self, f: ti.i32):
13781386
pos_j = ti.cast(self.elements_v[f, i_v, i_b].pos[j], ti.f32)
13791387
self.surface_render_v[i_v, i_b].vertices[j] = pos_j + self.envs_offset[i_b][j]
13801388

1381-
for i_s, i_b in ti.ndrange(self.n_surfaces, self._B):
1389+
# Fill triangle indices (flat array, 3 ints per triangle)
1390+
for i_s in range(self.n_surfaces):
13821391
for j in ti.static(range(3)):
13831392
self.surface_render_f[i_s * 3 + j].indices = ti.cast(self.surface[i_s].tri2v[j], ti.i32)
13841393

genesis/utils/element.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,21 @@ def cylinder_to_elements():
3131
raise NotImplementedError
3232

3333

34-
def mesh_to_elements(file, pos=(0, 0, 0), scale=1.0, tet_cfg=dict()):
35-
mesh = mu.load_mesh(file)
34+
def mesh_to_elements(file, pos=(0, 0, 0), scale=1.0, tet_cfg=dict(), return_uvs=False):
35+
# Load mesh (with texture data if UVs are requested)
36+
mesh = mu.load_mesh(file, skip_texture=not return_uvs)
3637
mesh.vertices = mesh.vertices * scale
38+
39+
# Extract UVs from mesh before tetrahedralization (if requested)
40+
# (tetgen preserves original vertices at start of output array)
41+
if return_uvs:
42+
n_original = len(mesh.vertices)
43+
try:
44+
original_uvs = mesh.visual.uv.copy()
45+
original_uvs[:, 1] = 1.0 - original_uvs[:, 1] # flip V (trimesh uses top-left origin)
46+
original_uvs = original_uvs.astype(gs.np_float)
47+
except AttributeError:
48+
original_uvs = None
3749

3850
# compute file name via hashing for caching
3951
tet_file_path = mu.get_tet_path(mesh.vertices, mesh.faces, tet_cfg)
@@ -43,8 +55,8 @@ def mesh_to_elements(file, pos=(0, 0, 0), scale=1.0, tet_cfg=dict()):
4355
if os.path.exists(tet_file_path):
4456
gs.logger.debug("Tetrahedra file (`.tet`) found in cache.")
4557
try:
46-
with open(tet_file_path, "rb") as file:
47-
verts, elems = pkl.load(file)
58+
with open(tet_file_path, "rb") as tet_file:
59+
verts, elems = pkl.load(tet_file)
4860
is_cached_loaded = True
4961
except (EOFError, ModuleNotFoundError, pkl.UnpicklingError, TypeError, MemoryError):
5062
gs.logger.info("Ignoring corrupted cache.")
@@ -54,10 +66,18 @@ def mesh_to_elements(file, pos=(0, 0, 0), scale=1.0, tet_cfg=dict()):
5466
verts, elems = mu.tetrahedralize_mesh(mesh, tet_cfg)
5567

5668
os.makedirs(os.path.dirname(tet_file_path), exist_ok=True)
57-
with open(tet_file_path, "wb") as file:
58-
pkl.dump((verts, elems), file)
69+
with open(tet_file_path, "wb") as tet_file:
70+
pkl.dump((verts, elems), tet_file)
5971

6072
verts += np.array(pos)
73+
74+
if return_uvs:
75+
# Build full UV array: original vertices get their UVs, interior vertices get zeros
76+
n_fem = len(verts)
77+
uvs = np.zeros((n_fem, 2), dtype=gs.np_float)
78+
if original_uvs is not None:
79+
uvs[:n_original] = original_uvs
80+
return verts, elems, uvs
6181

6282
return verts, elems
6383

genesis/utils/mesh.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,9 @@ def get_hashkey(*args):
169169
return hasher.hexdigest()
170170

171171

172-
def load_mesh(file):
172+
def load_mesh(file, skip_texture=True):
173173
if isinstance(file, (str, Path)):
174-
return trimesh.load(file, force="mesh", skip_texture=True)
174+
return trimesh.load(file, force="mesh", skip_texture=skip_texture)
175175
return file
176176

177177

0 commit comments

Comments
 (0)