diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index b906922b291..54330358d01 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -145,6 +145,7 @@ Guidelines for modifications: * Xavier Nal * Xinjie Yao * Xinpeng Liu +* Xin Xu * Yang Jin * Yanzi Zhu * Yijie Guo diff --git a/source/isaaclab/isaaclab/terrains/height_field/hf_terrains.py b/source/isaaclab/isaaclab/terrains/height_field/hf_terrains.py index d9ff255c3b5..2c450afee55 100644 --- a/source/isaaclab/isaaclab/terrains/height_field/hf_terrains.py +++ b/source/isaaclab/isaaclab/terrains/height_field/hf_terrains.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -11,7 +11,7 @@ import scipy.interpolate as interpolate from typing import TYPE_CHECKING -from .utils import height_field_to_mesh +from .utils import height_field_to_mesh, height_field_to_mesh_v2 if TYPE_CHECKING: from . import hf_terrains_cfg @@ -79,7 +79,7 @@ def random_uniform_terrain(difficulty: float, cfg: hf_terrains_cfg.HfRandomUnifo return np.rint(z_upsampled).astype(np.int16) -@height_field_to_mesh +@height_field_to_mesh_v2(terrain_origin_judge_width=1.0) def pyramid_sloped_terrain(difficulty: float, cfg: hf_terrains_cfg.HfPyramidSlopedTerrainCfg) -> np.ndarray: """Generate a terrain with a truncated pyramid structure. diff --git a/source/isaaclab/isaaclab/terrains/height_field/utils.py b/source/isaaclab/isaaclab/terrains/height_field/utils.py index 42ae15693e9..94f98fecc3f 100644 --- a/source/isaaclab/isaaclab/terrains/height_field/utils.py +++ b/source/isaaclab/isaaclab/terrains/height_field/utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -76,6 +76,59 @@ def wrapper(difficulty: float, cfg: HfTerrainBaseCfg): return wrapper +def height_field_to_mesh_v2(terrain_origin_judge_width: float = 2.0): + """ + the terrain origin is computed as the max in the square area around the center of the terrain, + the square width is `terrain_origin_judge_width` + """ + + def decorator(func): + @functools.wraps(func) + def wrapper(difficulty: float, cfg: HfTerrainBaseCfg): + # check valid border width + if cfg.border_width > 0 and cfg.border_width < cfg.horizontal_scale: + raise ValueError( + f"The border width ({cfg.border_width}) must be greater than or equal to the" + f" horizontal scale ({cfg.horizontal_scale})." + ) + # allocate buffer for height field (with border) + width_pixels = int(cfg.size[0] / cfg.horizontal_scale) + 1 + length_pixels = int(cfg.size[1] / cfg.horizontal_scale) + 1 + border_pixels = int(cfg.border_width / cfg.horizontal_scale) + 1 + heights = np.zeros((width_pixels, length_pixels), dtype=np.int16) + # override size of the terrain to account for the border + sub_terrain_size = [width_pixels - 2 * border_pixels, length_pixels - 2 * border_pixels] + sub_terrain_size = [dim * cfg.horizontal_scale for dim in sub_terrain_size] + # update the config + terrain_size = copy.deepcopy(cfg.size) + cfg.size = tuple(sub_terrain_size) + # generate the height field + z_gen = func(difficulty, cfg) + # handle the border for the terrain + heights[border_pixels:-border_pixels, border_pixels:-border_pixels] = z_gen + # set terrain size back to config + cfg.size = terrain_size + + # convert to trimesh + vertices, triangles = convert_height_field_to_mesh( + heights, cfg.horizontal_scale, cfg.vertical_scale, cfg.slope_threshold + ) + mesh = trimesh.Trimesh(vertices=vertices, faces=triangles) + # compute origin + x1 = int((cfg.size[0] * 0.5 - terrain_origin_judge_width / 2.0) / cfg.horizontal_scale) + x2 = int((cfg.size[0] * 0.5 + terrain_origin_judge_width / 2.0) / cfg.horizontal_scale) + y1 = int((cfg.size[1] * 0.5 - terrain_origin_judge_width / 2.0) / cfg.horizontal_scale) + y2 = int((cfg.size[1] * 0.5 + terrain_origin_judge_width / 2.0) / cfg.horizontal_scale) + origin_z = np.max(heights[x1:x2, y1:y2]) * cfg.vertical_scale + origin = np.array([0.5 * cfg.size[0], 0.5 * cfg.size[1], origin_z]) + # return mesh and origin + return [mesh], origin + + return wrapper + + return decorator + + def convert_height_field_to_mesh( height_field: np.ndarray, horizontal_scale: float, vertical_scale: float, slope_threshold: float | None = None ) -> tuple[np.ndarray, np.ndarray]: