Skip to content

Commit 253dc10

Browse files
committed
Merge branch 'main' into bvh
2 parents 5fad9b7 + 60c1935 commit 253dc10

File tree

13 files changed

+390
-137
lines changed

13 files changed

+390
-137
lines changed

genesis/engine/entities/hybrid_entity.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -431,8 +431,8 @@ def _kernel_update_soft_part_mpm(self, f: ti.i32):
431431
acc = vel_d / dt_for_rigid_acc
432432
frc_vel = mass_real * acc
433433
frc_ang = (x_pos - link.COM).cross(frc_vel)
434-
self._solver_rigid.links_state[link_idx, i_b].cfrc_ext_vel += frc_vel
435-
self._solver_rigid.links_state[link_idx, i_b].cfrc_ext_ang += frc_ang
434+
self._solver_rigid.links_state[link_idx, i_b].cfrc_applied_vel += frc_vel
435+
self._solver_rigid.links_state[link_idx, i_b].cfrc_applied_ang += frc_ang
436436

437437
# rigid-to-soft coupling # NOTE: this may lead to unstable feedback loop
438438
self._solver_soft.particles[f_, i_global, i_b].vel += vel_d * self.material.soft_dv_coef

genesis/engine/scene.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
import numpy as np
44
import torch
5+
import pickle
6+
import time
7+
import taichi as ti
58

69
import genesis as gs
710
import genesis.utils.geom as gu
@@ -1057,6 +1060,69 @@ def _backward(self):
10571060
self._backward_ready = False
10581061
self._forward_ready = False
10591062

1063+
def dump_ckpt_to_numpy(self) -> dict[str, np.ndarray]:
1064+
"""
1065+
Collect every Taichi field in the **scene and its active solvers** and
1066+
return them as a flat ``{key: ndarray}`` dictionary.
1067+
1068+
Returns
1069+
-------
1070+
dict[str, np.ndarray]
1071+
Mapping ``"Class.attr[.member]" → array`` with raw field data.
1072+
"""
1073+
arrays: dict[str, np.ndarray] = {}
1074+
1075+
for name, field in self.__dict__.items():
1076+
if isinstance(field, ti.Field):
1077+
arrays[".".join((self.__class__.__name__, name))] = field.to_numpy()
1078+
1079+
for solver in self.active_solvers:
1080+
arrays.update(solver.dump_ckpt_to_numpy())
1081+
1082+
return arrays
1083+
1084+
def save_checkpoint(self, path: str | os.PathLike) -> None:
1085+
"""
1086+
Pickle the full physics state to *one* file.
1087+
1088+
Parameters
1089+
----------
1090+
path : str | os.PathLike
1091+
Destination filename.
1092+
"""
1093+
state = {
1094+
"timestamp": time.time(),
1095+
"step_index": self.t,
1096+
"arrays": self.dump_ckpt_to_numpy(),
1097+
}
1098+
with open(path, "wb") as f:
1099+
pickle.dump(state, f, protocol=pickle.HIGHEST_PROTOCOL)
1100+
1101+
def load_checkpoint(self, path: str | os.PathLike) -> None:
1102+
"""
1103+
Restore a file produced by :py:meth:`save_checkpoint`.
1104+
1105+
Parameters
1106+
----------
1107+
path : str | os.PathLike
1108+
Path to the checkpoint pickle.
1109+
"""
1110+
with open(path, "rb") as f:
1111+
state = pickle.load(f)
1112+
1113+
arrays = state["arrays"]
1114+
1115+
for name, field in self.__dict__.items():
1116+
if isinstance(field, ti.Field):
1117+
key = ".".join((self.__class__.__name__, name))
1118+
if key in arrays:
1119+
field.from_numpy(arrays[key])
1120+
1121+
for solver in self.active_solvers:
1122+
solver.load_ckpt_from_numpy(arrays)
1123+
1124+
self._t = state.get("step_index", self._t)
1125+
10601126
# ------------------------------------------------------------------------------------
10611127
# ----------------------------------- properties -------------------------------------
10621128
# ------------------------------------------------------------------------------------

genesis/engine/solvers/base_solver.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from typing import TYPE_CHECKING
22
import numpy as np
33
import taichi as ti
4+
import torch
5+
from genesis.utils.misc import ti_field_to_torch
46

57
import genesis as gs
68
from genesis.engine.entities.base_entity import Entity
@@ -34,6 +36,53 @@ def __init__(self, scene: "Scene", sim: "Simulator", options):
3436
def _add_force_field(self, force_field):
3537
self._ffs.append(force_field)
3638

39+
def dump_ckpt_to_numpy(self) -> dict[str, np.ndarray]:
40+
arrays: dict[str, np.ndarray] = {}
41+
42+
for attr_name, field in self.__dict__.items():
43+
if not isinstance(field, ti.Field):
44+
continue
45+
46+
key_base = ".".join((self.__class__.__name__, attr_name))
47+
data = field.to_numpy()
48+
49+
# StructField → data is a dict: flatten each member
50+
if isinstance(data, dict):
51+
for sub_name, sub_arr in data.items():
52+
arrays[f"{key_base}.{sub_name}"] = (
53+
sub_arr if isinstance(sub_arr, np.ndarray) else np.asarray(sub_arr)
54+
)
55+
else:
56+
arrays[key_base] = data if isinstance(data, np.ndarray) else np.asarray(data)
57+
58+
return arrays
59+
60+
def load_ckpt_from_numpy(self, arr_dict: dict[str, np.ndarray]) -> None:
61+
for attr_name, field in self.__dict__.items():
62+
if not isinstance(field, ti.Field):
63+
continue
64+
65+
key_base = ".".join((self.__class__.__name__, attr_name))
66+
member_prefix = key_base + "."
67+
68+
# ---- StructField: gather its members -----------------------------
69+
member_items = {}
70+
for saved_key, saved_arr in arr_dict.items():
71+
if saved_key.startswith(member_prefix):
72+
sub_name = saved_key[len(member_prefix) :]
73+
member_items[sub_name] = saved_arr
74+
75+
if member_items: # we found at least one sub-member
76+
field.from_numpy(member_items)
77+
continue
78+
79+
# ---- Ordinary field ---------------------------------------------
80+
if key_base not in arr_dict:
81+
continue # nothing saved for this attribute
82+
83+
arr = arr_dict[key_base]
84+
field.from_numpy(arr)
85+
3786
# ------------------------------------------------------------------------------------
3887
# ----------------------------------- properties -------------------------------------
3988
# ------------------------------------------------------------------------------------

genesis/options/morphs.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -893,6 +893,8 @@ class Terrain(Morph):
893893
The size of each cell in the subterrain in meters. Defaults to 0.25.
894894
vertical_scale : float, optional
895895
The height of each step in the subterrain in meters. Defaults to 0.005.
896+
uv_scale : float, optional
897+
The scale of the UV mapping for the terrain. Defaults to 1.0.
896898
subterrain_types : str or 2D list of str, optional
897899
The types of subterrains to generate. If a string, it will be repeated for all subterrains. If a 2D list, it should have the same shape as `n_subterrains`.
898900
height_field : array-like, optional
@@ -911,6 +913,7 @@ class Terrain(Morph):
911913
subterrain_size: Tuple[float, float] = (12.0, 12.0) # meter
912914
horizontal_scale: float = 0.25 # meter size of each cell in the subterrain
913915
vertical_scale: float = 0.005 # meter height of each step in the subterrain
916+
uv_scale: float = 1.0
914917
subterrain_types: Any = [
915918
["flat_terrain", "random_uniform_terrain", "stepping_stones_terrain"],
916919
["pyramid_sloped_terrain", "discrete_obstacles_terrain", "wave_terrain"],

genesis/utils/geom.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -734,11 +734,7 @@ def quat_to_xyz(quat, rpy=False, degrees=False):
734734
sinp = 2 * (qw * qy - qz * qx)
735735
else:
736736
sinp = 2 * (qx * qz + qw * qy)
737-
pitch = torch.where(
738-
torch.abs(sinp) >= 1,
739-
torch.sign(sinp) * torch.tensor(torch.pi / 2),
740-
torch.asin(sinp),
741-
)
737+
pitch = torch.where(torch.abs(sinp) >= 1, torch.sign(sinp) * (torch.pi / 2), torch.asin(sinp))
742738

743739
# Yaw (z-axis rotation)
744740
if rpy:

genesis/utils/misc.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,8 @@ def get_device(backend: gs_backend):
179179
gs.raise_exception("torch cuda not available")
180180

181181
device_idx = torch.cuda.current_device()
182-
device = torch.device(f"cuda:{device_idx}")
183-
device_property = torch.cuda.get_device_properties(device_idx)
182+
device = torch.device("cuda", device_idx)
183+
device_property = torch.cuda.get_device_properties(device)
184184
device_name = device_property.name
185185
total_mem = device_property.total_memory / 1024**3
186186

@@ -190,14 +190,14 @@ def get_device(backend: gs_backend):
190190

191191
# on mac, cpu and gpu are in the same device
192192
_, device_name, total_mem, _ = get_device(gs_backend.cpu)
193-
device = torch.device("mps:0")
193+
device = torch.device("mps", 0)
194194

195195
elif backend == gs_backend.vulkan:
196196
if torch.cuda.is_available():
197197
device, device_name, total_mem, _ = get_device(gs_backend.cuda)
198198
elif torch.xpu.is_available(): # pytorch 2.5+ supports Intel XPU device
199199
device_idx = torch.xpu.current_device()
200-
device = torch.device(f"xpu:{device_idx}")
200+
device = torch.device("xpu", device_idx)
201201
device_property = torch.xpu.get_device_properties(device_idx)
202202
device_name = device_property.name
203203
total_mem = device_property.total_memory / 1024**3

genesis/utils/terrain.py

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,12 @@ def parse_terrain(morph: Terrain, surface):
150150
i * subterrain_rows : (i + 1) * subterrain_rows, j * subterrain_cols : (j + 1) * subterrain_cols
151151
] = subterrain_height_field
152152

153+
need_uvs = getattr(surface, "diffuse_texture", None) is not None
153154
tmesh, sdf_tmesh = convert_heightfield_to_watertight_trimesh(
154155
heightfield,
155156
horizontal_scale=morph.horizontal_scale,
156157
vertical_scale=morph.vertical_scale,
158+
uv_scale=morph.uv_scale if need_uvs else None,
157159
)
158160

159161
terrain_dir = os.path.join(get_assets_dir(), f"terrain/{morph.name}")
@@ -169,7 +171,7 @@ def parse_terrain(morph: Terrain, surface):
169171
}
170172
pickle.dump(info, f)
171173

172-
vmesh = gs.Mesh.from_trimesh(mesh=tmesh, surface=surface)
174+
vmesh = gs.Mesh.from_trimesh(mesh=tmesh, surface=surface, metadata={})
173175
mesh = gs.Mesh.from_trimesh(
174176
mesh=tmesh,
175177
surface=gs.surfaces.Collision(),
@@ -211,7 +213,9 @@ def fractal_terrain(terrain, levels=8, scale=1.0):
211213
return terrain
212214

213215

214-
def convert_heightfield_to_watertight_trimesh(height_field_raw, horizontal_scale, vertical_scale, slope_threshold=None):
216+
def convert_heightfield_to_watertight_trimesh(
217+
height_field_raw, horizontal_scale, vertical_scale, slope_threshold=None, uv_scale=None
218+
):
215219
"""
216220
Adapted from Issac Gym's `convert_heightfield_to_trimesh` function.
217221
Convert a heightfield array to a triangle mesh represented by vertices and triangles.
@@ -263,8 +267,8 @@ def convert_heightfield_to_watertight_trimesh(height_field_raw, horizontal_scale
263267

264268
# create triangle mesh vertices and triangles from the heightfield grid
265269
vertices_top = np.zeros((num_rows * num_cols, 3), dtype=np.float32)
266-
vertices_top[:, 0] = xx.flatten()
267-
vertices_top[:, 1] = yy.flatten()
270+
vertices_top[:, 0] = xx.flat
271+
vertices_top[:, 1] = yy.flat
268272
vertices_top[:, 2] = hf.flatten() * vertical_scale
269273
triangles_top = -np.ones((2 * (num_rows - 1) * (num_cols - 1), 3), dtype=np.uint32)
270274
for i in range(num_rows - 1):
@@ -285,8 +289,8 @@ def convert_heightfield_to_watertight_trimesh(height_field_raw, horizontal_scale
285289
z_min = np.min(vertices_top[:, 2]) - 1.0
286290

287291
vertices_bottom = np.zeros((num_rows * num_cols, 3), dtype=np.float32)
288-
vertices_bottom[:, 0] = xx.flatten()
289-
vertices_bottom[:, 1] = yy.flatten()
292+
vertices_bottom[:, 0] = xx.flat
293+
vertices_bottom[:, 1] = yy.flat
290294
vertices_bottom[:, 2] = z_min
291295
triangles_bottom = -np.ones((2 * (num_rows - 1) * (num_cols - 1), 3), dtype=np.uint32)
292296
for i in range(num_rows - 1):
@@ -347,15 +351,45 @@ def convert_heightfield_to_watertight_trimesh(height_field_raw, horizontal_scale
347351
axis=0,
348352
)
349353

350-
# This a uniformly-distributed full mesh, which gives faster sdf generation
351-
sdf_mesh = trimesh.Trimesh(vertices, triangles, process=False)
354+
if uv_scale is not None:
355+
uv_top = np.zeros((num_rows * num_cols, 2), dtype=np.float32)
356+
uv_top[:, 0] = (xx.flat - xx.min()) / (xx.max() - xx.min()) * uv_scale
357+
uv_top[:, 1] = (yy.flat - yy.min()) / (yy.max() - yy.min()) * uv_scale
358+
359+
uvs = np.concatenate([uv_top, uv_top], axis=0)
360+
visual = trimesh.visual.TextureVisuals(uv=uvs)
361+
else:
362+
uvs = None
363+
visual = None
364+
365+
sdf_mesh = trimesh.Trimesh(vertices, triangles, process=False, visual=visual)
352366

353367
# This is the mesh used for non-sdf purposes.
354368
# It's losslessly simplified from the full mesh, to save memory cost for storing verts and faces.
355-
mesh = trimesh.Trimesh(
356-
*fast_simplification.simplify(sdf_mesh.vertices, sdf_mesh.faces, target_count=0, lossless=True)
369+
370+
v_simp, f_simp = fast_simplification.simplify(
371+
sdf_mesh.vertices,
372+
sdf_mesh.faces,
373+
target_count=0,
374+
lossless=True,
357375
)
358376

377+
if uvs is not None:
378+
idx_map = np.empty(len(v_simp), dtype=np.int64)
379+
for i, v in enumerate(v_simp):
380+
dists = np.square(vertices - v).sum(axis=1)
381+
idx_map[i] = np.argmin(dists)
382+
383+
uv_simp = uvs[idx_map]
384+
385+
mesh = trimesh.Trimesh(
386+
v_simp,
387+
f_simp,
388+
visual=trimesh.visual.TextureVisuals(uv=uv_simp),
389+
)
390+
else:
391+
mesh = trimesh.Trimesh(v_simp, f_simp)
392+
359393
return mesh, sdf_mesh
360394

361395

0 commit comments

Comments
 (0)