diff --git a/README.md b/README.md index 44379e6..fc75ae7 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ python deploy/deploy_mujoco/deploy_go2.py Connect an Xbox-compatible gamepad to enable teleoperation; otherwise, the agent keeps a default forward command. - **Swap the policy**: The default checkpoint is `deploy/pre_train/go2/go2_cts_150k.pt`. Replace `policy_path` in the YAML config with your own `logs/{experiment_name}/exported/policies/policy.pt`. -- **Swap terrains**: Default terrain is `resources/robots/go2/stairs.xml`. Alternatives include `flat.xml` and `race_track.xml`. Generate new terrains with [terrain_generator.py](resources/robots/go2/terrain_generator.py) (see also [unitree_mujoco/terrain_tool](https://github.com/unitreerobotics/unitree_mujoco/tree/main/terrain_tool)). +- **Swap terrains**: Default terrain is `resources/robots/go2/stairs.xml`. Alternatives include `flat.xml`, `race_track.xml`, `cross_stairs.xml`, and `cross_slope.xml`. Generate new terrains with [windigal - mujoco_terrains](https://github.com/windigal/mujoco_terrains) #### Results @@ -159,7 +159,7 @@ Related publications implemented in this repo: - [CTS: Concurrent Teacher-Student Reinforcement Learning for Legged Locomotion](https://arxiv.org/pdf/2405.10830) Contributors: -- [@windigal](https://github.com/windigal): CTS algorithm reproduction, video editing +- [@windigal](https://github.com/windigal): CTS algorithm reproduction, terrain generation, video editing - [@wertyuilife2](https://github.com/wertyuilife2): CTS algorithm reproduction --- diff --git a/README_zh.md b/README_zh.md index b200dab..2b1606c 100644 --- a/README_zh.md +++ b/README_zh.md @@ -94,7 +94,7 @@ python deploy/deploy_mujoco/deploy_go2.py 如果有xbox协议的手柄接入主机,自动切换为手柄控制,否则只会保持默认指令前进。 - 替换网络模型:默认模型位于 `deploy/pre_train/go2/go2_cts_150k.pt`;自己训练模型保存于`logs/{experiment_name}/exported/policies/policy.pt`,只需替换 yaml 配置文件中 `policy_path`。 -- 替换环境地形:默认地形为 `resources/robots/go2/stairs.xml`,其他可选地形,平地 `flat.xml`,赛道 `race_track.xml`,地形使用[terrain_generator.py](resources/robots/go2/terrain_generator.py)生成,参考[unitree_mujoco/terrain_tool](https://github.com/unitreerobotics/unitree_mujoco/tree/main/terrain_tool)。 +- 替换环境地形:默认地形为 `resources/robots/go2/stairs.xml`,其他可选地形,平地 `flat.xml`,赛道 `race_track.xml`,交叉楼梯和斜坡`cross_stairs`/`cross_slope`,地形使用[windigal - mujoco_terrains](https://github.com/windigal/mujoco_terrains)生成 #### 运行效果 @@ -157,7 +157,7 @@ python deploy_real_go2.py eth0 - [CTS: Concurrent Teacher-Student Reinforcement Learning for Legged Locomotion](https://arxiv.org/pdf/2405.10830) 贡献者: -- [@windigal](https://github.com/windigal):复现CTS算法,剪辑视频 +- [@windigal](https://github.com/windigal):复现CTS算法,生成地形,剪辑视频 - [@wertyuilife2](https://github.com/wertyuilife2):复现CTS算法 --- diff --git a/deploy/deploy_mujoco/configs/go2.yaml b/deploy/deploy_mujoco/configs/go2.yaml index 3093373..fb47b0e 100644 --- a/deploy/deploy_mujoco/configs/go2.yaml +++ b/deploy/deploy_mujoco/configs/go2.yaml @@ -2,8 +2,10 @@ policy_path: "{LEGGED_GYM_ROOT_DIR}/deploy/pre_train/go2/go2_moe_cts_137000_0.63 # policy_path: "{LEGGED_GYM_ROOT_DIR}/deploy/pre_train/go2/go2_cts_150k.pt" # xml_path: "{LEGGED_GYM_ROOT_DIR}/resources/robots/go2/flat.xml" -# xml_path: "{LEGGED_GYM_ROOT_DIR}/resources/robots/go2/race_track.xml" # change go2 init pos to pos="-5 2 0.445" in resources/robots/go2/go2.xml +# xml_path: "{LEGGED_GYM_ROOT_DIR}/resources/robots/go2/race_track.xml" xml_path: "{LEGGED_GYM_ROOT_DIR}/resources/robots/go2/stairs.xml" +# xml_path: "{LEGGED_GYM_ROOT_DIR}/resources/robots/go2/cross_slope.xml" +# xml_path: "{LEGGED_GYM_ROOT_DIR}/resources/robots/go2/cross_stairs.xml" # Total simulation time simulation_duration: 60000000.0 diff --git a/resources/robots/go2/cross_slope.xml b/resources/robots/go2/cross_slope.xml new file mode 100644 index 0000000..f5cf284 --- /dev/null +++ b/resources/robots/go2/cross_slope.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/robots/go2/cross_stairs.xml b/resources/robots/go2/cross_stairs.xml new file mode 100644 index 0000000..ec1e85e --- /dev/null +++ b/resources/robots/go2/cross_stairs.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/robots/go2/imgs/label_1.png b/resources/robots/go2/imgs/label_1.png new file mode 100644 index 0000000..4fc8ee7 Binary files /dev/null and b/resources/robots/go2/imgs/label_1.png differ diff --git a/resources/robots/go2/imgs/label_10.png b/resources/robots/go2/imgs/label_10.png new file mode 100644 index 0000000..191908b Binary files /dev/null and b/resources/robots/go2/imgs/label_10.png differ diff --git a/resources/robots/go2/imgs/label_4.png b/resources/robots/go2/imgs/label_4.png new file mode 100644 index 0000000..b80ad7c Binary files /dev/null and b/resources/robots/go2/imgs/label_4.png differ diff --git a/resources/robots/go2/imgs/label_7.png b/resources/robots/go2/imgs/label_7.png new file mode 100644 index 0000000..e5414ac Binary files /dev/null and b/resources/robots/go2/imgs/label_7.png differ diff --git a/resources/robots/go2/race_track.xml b/resources/robots/go2/race_track.xml index 6cdeacb..0a7814e 100644 --- a/resources/robots/go2/race_track.xml +++ b/resources/robots/go2/race_track.xml @@ -1,22 +1,22 @@ - + - + - + - + - + - - + + - + - + \ No newline at end of file diff --git a/resources/robots/go2/race_track_generator.py b/resources/robots/go2/race_track_generator.py deleted file mode 100644 index e84c22c..0000000 --- a/resources/robots/go2/race_track_generator.py +++ /dev/null @@ -1,808 +0,0 @@ -import xml.etree.ElementTree as xml_et -from pathlib import Path -import numpy as np -import cv2 -import noise -import os - -ROBOT = "go2" -INPUT_SCENE_PATH = os.path.join(os.path.dirname(__file__), "flat.xml") -OUTPUT_SCENE_PATH = os.path.join(os.path.dirname(__file__), "race_track_tmp.xml") -PATH_DIR = Path(__file__).parent.absolute() -# zyx euler angle to quaternion -def euler_to_quat(roll, pitch, yaw): - cx = np.cos(roll / 2) - sx = np.sin(roll / 2) - cy = np.cos(pitch / 2) - sy = np.sin(pitch / 2) - cz = np.cos(yaw / 2) - sz = np.sin(yaw / 2) - - return np.array( - [ - cx * cy * cz + sx * sy * sz, - sx * cy * cz - cx * sy * sz, - cx * sy * cz + sx * cy * sz, - cx * cy * sz - sx * sy * cz, - ], - dtype=np.float64, - ) - - -# zyx euler angle to rotation matrix -def euler_to_rot(roll, pitch, yaw): - rot_x = np.array( - [ - [1, 0, 0], - [0, np.cos(roll), -np.sin(roll)], - [0, np.sin(roll), np.cos(roll)], - ], - dtype=np.float64, - ) - - rot_y = np.array( - [ - [np.cos(pitch), 0, np.sin(pitch)], - [0, 1, 0], - [-np.sin(pitch), 0, np.cos(pitch)], - ], - dtype=np.float64, - ) - rot_z = np.array( - [ - [np.cos(yaw), -np.sin(yaw), 0], - [np.sin(yaw), np.cos(yaw), 0], - [0, 0, 1], - ], - dtype=np.float64, - ) - return rot_z @ rot_y @ rot_x - - -# 2d rotate -def rot2d(x, y, yaw): - nx = x * np.cos(yaw) - y * np.sin(yaw) - ny = x * np.sin(yaw) + y * np.cos(yaw) - return nx, ny - - -# 3d rotate -def rot3d(pos, euler): - R = euler_to_rot(euler[0], euler[1], euler[2]) - return R @ pos - - -def list_to_str(vec): - return " ".join(str(s) for s in vec) - - -class TerrainGenerator: - - def __init__(self) -> None: - self.scene = xml_et.parse(INPUT_SCENE_PATH) - self.root = self.scene.getroot() - self.worldbody = self.root.find("worldbody") - self.asset = self.root.find("asset") - self._add_wood_material() - self._add_sponge_material() - - def _add_wood_material(self): - # 1. 添加纹理 (Texture) - # 这里使用内置的 "flat" 类型配上棕色,模拟一种简单的木头颜色。 - # 如果你有真实的木纹图片(比如 wood.png),请将 type="2d" builtin="flat" - # 改为 type="2d" file="../wood.png" - tex = xml_et.SubElement(self.asset, "texture") - tex.attrib["name"] = "wood_tex" - tex.attrib["type"] = "2d" - tex.attrib["file"] = "./assets/wood.png" - tex.attrib["rgb1"] = "0.6 0.4 0.2" # 棕色 (RGB) - tex.attrib["width"] = "512" - tex.attrib["height"] = "512" - - # 2. 添加材质 (Material) - mat = xml_et.SubElement(self.asset, "material") - mat.attrib["name"] = "wood_mat" # 材质名称,后面 AddBox 要用 - mat.attrib["texture"] = "wood_tex" # 关联上面的纹理 - mat.attrib["specular"] = "0.2" # 木头反光度较低 - mat.attrib["shininess"] = "0.1" # 亮度较低 - mat.attrib["rgba"] = "1 1 1 1" - - def _add_sponge_material(self): - # 1. 添加纹理 (Texture) - # 因为没有图片文件,我们使用 builtin="flat" 来生成纯色纹理 - tex = xml_et.SubElement(self.asset, "texture") - tex.attrib["name"] = "sponge_tex" - tex.attrib["type"] = "2d" - tex.attrib["builtin"] = "flat" # 使用内置平面纹理,不需要 file 路径 - # tex.attrib["rgb1"] = "1.0 0.7 0.7" # 设置颜色:粉色 (参考图片颜色) - tex.attrib["rgb1"] = "0.90196 0.83922 0.56471" # 设置颜色:黄色 - tex.attrib["width"] = "512" - tex.attrib["height"] = "512" - - # 2. 添加材质 (Material) - mat = xml_et.SubElement(self.asset, "material") - mat.attrib["name"] = "mat_sponge" # 材质名称,AddBox 中调用这个名字 - mat.attrib["texture"] = "sponge_tex" - - # 海绵的关键视觉特性:不反光、不油亮 - mat.attrib["specular"] = "0.1" # 几乎没有镜面反射 (相比木头的0.2要低很多) - mat.attrib["shininess"] = "0.1" # 几乎没有光泽 - mat.attrib["rgba"] = "1 0.7 0.7 1" # 叠加颜色,保持原样 - - # Add Box to scene - def AddBox(self, - position=[1.0, 0.0, 0.0], - euler=[0.0, 0.0, 0.0], - size=[0.1, 0.1, 0.1], - sponge=False): - geo = xml_et.SubElement(self.worldbody, "geom") - geo.attrib["pos"] = list_to_str(position) - geo.attrib["type"] = "box" - geo.attrib["size"] = list_to_str( - 0.5 * np.array(size)) # half size of box for mujoco - quat = euler_to_quat(euler[0], euler[1], euler[2]) - geo.attrib["quat"] = list_to_str(quat) - # === 修改部分开始 === - if sponge: - # 1. 视觉:使用海绵材质 (假设你在 asset 中定义的名字叫 mat_sponge) - geo.attrib["material"] = "mat_sponge" - # 2. 物理:solref 时间常数越大越软 (0.02 比较软, 默认约 0.002) - geo.attrib["solref"] = "0.03 1" - geo.attrib["priority"] = "1" - geo.attrib["solmix"] = "1" - # 3. 摩擦:海绵通常摩擦力较大 (可选) - geo.attrib["friction"] = "1.2 0.005 0.0001" - else: - geo.attrib["material"] = "wood_mat" - geo.attrib["friction"] = "0.5 0.005 0.0001" - - def AddGeometry(self, - position=[1.0, 0.0, 0.0], - euler=[0.0, 0.0, 0.0], - size=[0.1, 0.1],geo_type="box"): - - # geo_type supports "plane", "sphere", "capsule", "ellipsoid", "cylinder", "box" - geo = xml_et.SubElement(self.worldbody, "geom") - geo.attrib["pos"] = list_to_str(position) - geo.attrib["type"] = geo_type - geo.attrib["size"] = list_to_str( - 0.5 * np.array(size)) # half size of box for mujoco - quat = euler_to_quat(euler[0], euler[1], euler[2]) - geo.attrib["quat"] = list_to_str(quat) - geo.attrib["material"] = "wood_mat" - - def AddStairs(self, - init_pos=[1.0, 0.0, 0.0], - yaw=0.0, - width=0.2, - height=0.15, - length=1.5, - stair_nums=10): - - local_pos = [0.0, 0.0, -0.5 * height] - for i in range(stair_nums): - local_pos[0] += width - local_pos[2] += height - x, y = rot2d(local_pos[0], local_pos[1], yaw) - self.AddBox([x + init_pos[0], y + init_pos[1], local_pos[2]], - [0.0, 0.0, yaw], [width, length, height]) - - - def AddDownStairs(self, - init_pos=[1.0, 0.0, 0.0], - yaw=0.0, - width=0.3, - height=0.15, - length=1.5, - stair_nums=10): - - # 从上向下生成台阶:第一个台阶中心在 +0.5*height,随后每步降低 height - local_pos = [0.0, 0.0, 0.5 * height] - for i in range(stair_nums): - local_pos[0] += width - local_pos[2] -= height - x, y = rot2d(local_pos[0], local_pos[1], yaw) - self.AddBox([x + init_pos[0], y + init_pos[1], local_pos[2]], - [0.0, 0.0, yaw], [width, length, height]) - - - def AddStairsSeries(self, - init_pos=[1.0, 0.0, 0.0], - yaw=0.0, - width=0.3, - length=1.5, - stair_nums_up=6, - stair_nums_down=6, - start_height=0.05, - step_inc=0.03, - max_height=0.2, - flat_length=0.5): - """生成一系列台阶:先上台阶(stair_nums_up),顶部有一段平地(flat_length),再下台阶(stair_nums_down)。 - 每完成一对上/下台阶后,单步高度增加 step_inc,直到达到 max_height。 - - 参数说明: - - init_pos: 底层起点(列表),序列沿局部 x 方向展开 - - yaw: 台阶朝向 - - width: 每级台阶在 x 方向的深度(步幅) - - length: 台阶在 y 方向的宽度(和 AddStairs 一致) - - stair_nums_up/down: 上/下台阶的级数 - - start_height: 第一对台阶的每级高度 - - step_inc: 每对增加的高度 - - max_height: 最大每级高度(包含) - - flat_length: 顶部平地长度(沿 x) - """ - - # 保持 init_pos 不变(上/下台阶成对结束后回到同一基准高度) - base_pos = np.array(init_pos, dtype=float) - height = start_height - - # 平台长度至少为 1.0 米 - platform_length = max(flat_length, 1.0) - - # 迭代每一对台阶直到高度超限 - while height <= max_height + 1e-8: - # --- 上台阶 --- - # 上台阶第 i 级中心 z = base_z + (-0.5 + i) * height, i = 1..stair_nums_up - for i in range(1, stair_nums_up + 1): - center_x_local = i * width - center_z = base_pos[2] + (-0.5 + i) * height - x, y = rot2d(center_x_local, 0.0, yaw) - self.AddBox([x + base_pos[0], y + base_pos[1], center_z], - [0.0, 0.0, yaw], [width, length, height]) - - # 顶部平地:放在最后一级之后,平台长度至少 platform_length - last_up_center_x = stair_nums_up * width - last_up_top_surface = base_pos[2] + stair_nums_up * height # 顶部平面高度 - flat_thickness = height # 平地厚度,使用与台阶同高度以保证接触 - flat_center_local_x = last_up_center_x + width / 2.0 + platform_length / 2.0 - flat_center_z = last_up_top_surface + flat_thickness / 2.0 - x, y = rot2d(flat_center_local_x, 0.0, yaw) - # 尺寸:在 x 方向用 platform_length, y 用 length, z 用 flat_thickness - self.AddBox([x + base_pos[0], y + base_pos[1], flat_center_z], - [0.0, 0.0, yaw], [platform_length, length, flat_thickness]) - - # --- 下台阶 --- - # 平地末端 x - flat_end_x = last_up_center_x + width / 2.0 + platform_length - for j in range(1, stair_nums_down + 1): - center_x_local = flat_end_x + width / 2.0 + (j - 1) * width - # 第 j 级下台阶的中心 z = last_up_top_surface - 0.5*height - (j-1)*height - center_z = last_up_top_surface - 0.5 * height - (j - 1) * height - x, y = rot2d(center_x_local, 0.0, yaw) - self.AddBox([x + base_pos[0], y + base_pos[1], center_z], - [0.0, 0.0, yaw], [width, length, height]) - - # 下台阶之后也添加一段平地(连接到下一组上台阶),长度至少 platform_length - seq_end_local_x = flat_end_x + stair_nums_down * width # 这是最后一个下台阶的前缘 x - post_flat_center_local_x = seq_end_local_x + platform_length / 2.0 - # 该平地应与下一组上台阶的起始高度对齐:其顶面与 base_z + height 对齐 - post_flat_center_z = base_pos[2] + height / 2.0 - x, y = rot2d(post_flat_center_local_x, 0.0, yaw) - self.AddBox([x + base_pos[0], y + base_pos[1], post_flat_center_z], - [0.0, 0.0, yaw], [platform_length, length, flat_thickness]) - - # 为下一对台阶准备:把 base_pos 在 x 方向平移到当前序列末端(post flat 末端),保持 z 不变 - seq_total_end_local_x = seq_end_local_x + platform_length - # 计算下一组基准位移,使下一组上台阶第一级的前缘与当前 post-flat 的末端无缝对接 - # base_shift_local_x 为相对于当前 base 的局部 x 偏移 - overlap = 1e-3 # 以米为单位,微小重叠以避免可视缝隙 - base_shift_local_x = seq_total_end_local_x - width / 2.0 - overlap - dx, dy = rot2d(base_shift_local_x, 0.0, yaw) - base_pos[0] = base_pos[0] + dx - base_pos[1] = base_pos[1] + dy - - # 增加单级高度 - height = round(height + step_inc, 8) - - - def AddSuspendStairs(self, - init_pos=[1.0, 0.0, 0.0], - yaw=1.0, - width=0.2, - height=0.15, - length=1.5, - gap=0.1, - stair_nums=10): - - local_pos = [0.0, 0.0, -0.5 * height] - for i in range(stair_nums): - local_pos[0] += width - local_pos[2] += height - x, y = rot2d(local_pos[0], local_pos[1], yaw) - self.AddBox([x + init_pos[0], y + init_pos[1], local_pos[2]], - [0.0, 0.0, yaw], - [width, length, abs(height - gap)]) - - def AddRoughGround(self, - init_pos=[1.0, 0.0, 0.0], - euler=[0.0, -0.0, 0.0], - nums=[10, 10], - box_size=[0.5, 0.5, 0.5], - box_euler=[0.0, 0.0, 0.0], - separation=[0.2, 0.2], - box_size_rand=[0.05, 0.05, 0.05], - box_euler_rand=[0.2, 0.2, 0.2], - separation_rand=[0.05, 0.05]): - - local_pos = [0.0, 0.0, -0.5 * box_size[2]] - new_separation = np.array(separation) + np.array( - separation_rand) * np.random.uniform(-1.0, 1.0, 2) - for i in range(nums[0]): - local_pos[0] += new_separation[0] - local_pos[1] = 0.0 - for j in range(nums[1]): - new_box_size = np.array(box_size) + np.array( - box_size_rand) * np.random.uniform(-1.0, 1.0, 3) - new_box_euler = np.array(box_euler) + np.array( - box_euler_rand) * np.random.uniform(-1.0, 1.0, 3) - new_separation = np.array(separation) + np.array( - separation_rand) * np.random.uniform(-1.0, 1.0, 2) - - local_pos[1] += new_separation[1] - pos = rot3d(local_pos, euler) + np.array(init_pos) - self.AddBox(pos, new_box_euler, new_box_size) - - def AddPerlinHeighField( - self, - position=[1.0, 0.0, 0.0], # position - euler=[0.0, -0.0, 0.0], # attitude - size=[1.0, 1.0], # width and length - height_scale=0.2, # max height - negative_height=0.2, # height in the negative direction of z axis - image_width=128, # height field image size - img_height=128, - smooth=100.0, # smooth scale - perlin_octaves=6, # perlin noise parameter - perlin_persistence=0.5, - perlin_lacunarity=2.0, - output_hfield_image="height_field.png"): - - # Generating height field based on perlin noise - terrain_image = np.zeros((img_height, image_width), dtype=np.uint8) - for y in range(image_width): - for x in range(image_width): - # Perlin noise - noise_value = noise.pnoise2(x / smooth, - y / smooth, - octaves=perlin_octaves, - persistence=perlin_persistence, - lacunarity=perlin_lacunarity) - terrain_image[y, x] = int((noise_value + 1) / 2 * 255) - - cv2.imwrite(str(PATH_DIR / "assets" / output_hfield_image), terrain_image) - - hfield = xml_et.SubElement(self.asset, "hfield") - hfield.attrib["name"] = "perlin_hfield" - hfield.attrib["size"] = list_to_str( - [size[0] / 2.0, size[1] / 2.0, height_scale, negative_height]) - hfield.attrib["file"] = "../" + output_hfield_image - - geo = xml_et.SubElement(self.worldbody, "geom") - geo.attrib["type"] = "hfield" - geo.attrib["hfield"] = "perlin_hfield" - geo.attrib["pos"] = list_to_str(position) - quat = euler_to_quat(euler[0], euler[1], euler[2]) - geo.attrib["quat"] = list_to_str(quat) - - def AddHeighFieldFromImage( - self, - position=[1.0, 0.0, 0.0], # position - euler=[0.0, -0.0, 0.0], # attitude - size=[2.0, 1.6], # width and length - height_scale=0.02, # max height - negative_height=0.1, # height in the negative direction of z axis - input_img=None, - output_hfield_image="height_field.png", - image_scale=[1.0, 1.0], # reduce image resolution - invert_gray=False): - - input_image = cv2.imread(input_img) # 替换为你的图像文件路径 - - width = int(input_image.shape[1] * image_scale[0]) - height = int(input_image.shape[0] * image_scale[1]) - resized_image = cv2.resize(input_image, (width, height), - interpolation=cv2.INTER_AREA) - terrain_image = cv2.cvtColor(resized_image, cv2.COLOR_BGR2GRAY) - if invert_gray: - terrain_image = 255 - position - cv2.imwrite(str(PATH_DIR / "assets" / output_hfield_image), terrain_image) - - hfield = xml_et.SubElement(self.asset, "hfield") - hfield.attrib["name"] = "image_hfield" - hfield.attrib["size"] = list_to_str( - [size[0] / 2.0, size[1] / 2.0, height_scale, negative_height]) - hfield.attrib["file"] = "../" + output_hfield_image - - geo = xml_et.SubElement(self.worldbody, "geom") - geo.attrib["type"] = "hfield" - geo.attrib["hfield"] = "image_hfield" - geo.attrib["pos"] = list_to_str(position) - quat = euler_to_quat(euler[0], euler[1], euler[2]) - geo.attrib["quat"] = list_to_str(quat) - - def Save(self): - self.scene.write(OUTPUT_SCENE_PATH) - - def AddSlope(self, - position=[1.0, 0.0, 0.0], - yaw=0.0, - length=0.575, - width=0.55, - height=0.15, - thickness=0, - add_baffle=True, - add_baffle_height=False, - sponge=False): - """ - 生成带后背板和两侧挡板的斜坡。 - 侧挡板上沿与斜坡面对齐。 - """ - if not thickness: - thickness = 0.05 # 斜坡面板厚度 - side_thickness = 0.05 # 侧板厚度 - - # =========================== - # 1. 计算公共几何参数 - # =========================== - - # 计算坡度角和斜边长 - angle = np.arctan2(height, length) - ramp_len = np.sqrt(length**2 + height**2) - - # 法线向量 (nx, nz) - nx = np.sin(angle) - nz = -np.cos(angle) - - # 斜坡面板的几何中心 (局部坐标) - # 这里的逻辑是将面板中心沿着法线向下偏移厚度的一半,保证上表面对齐理想斜面 - mid_x = length / 2.0 - mid_z = height / 2.0 - lx = mid_x + nx * (thickness / 2.0) - lz = mid_z + nz * (thickness / 2.0) - - # 统一的旋转角度 (Pitch: -angle, Yaw: yaw) - final_euler = [0.0, -angle, yaw] - - # =========================== - # 2. 生成主斜坡面 (Ramp) - # =========================== - gx, gy = rot2d(lx, 0, yaw) - ramp_pos = [position[0] + gx, position[1] + gy, position[2] + lz] - self.AddBox(ramp_pos, final_euler, [ramp_len, width, thickness],sponge=sponge) - - # =========================== - # 3. 生成垂直背板 (Back Wall) - # =========================== - # 位于斜坡末端,高度为 height - if add_baffle: - back_lx = length + (thickness / 2.0) - if add_baffle_height: - back_lz = height - height *= 2 - else: - back_lz = height / 2.0 - bgx, bgy = rot2d(back_lx, 0, yaw) - back_pos = [position[0] + bgx, position[1] + bgy, position[2] + back_lz] - # 背板竖直放置,只受Yaw影响 - self.AddBox(back_pos, [0, 0, yaw], [thickness, width, height]) - - # 生成两侧挡板 (Side Walls) - side_h = height / 2 - - # 计算侧板的中心 Z 坐标 (side_lz) - # 目标:侧板的上表面 Z = 斜坡的上表面 Z - # 斜坡上表面 Z (局部) = lz + thickness/2 - # 侧板上表面 Z (局部) = side_lz + side_h/2 - # 等式:lz + thickness/2 = side_lz + side_h/2 - # 解得: - side_lz = lz + (thickness / 2.0) - (side_h / 2.0) - if add_baffle_height: - side_lz += side_h - # side_h *= 2 - # lx += height * 2 / length / 2 - # 侧板的 X 坐标与斜坡中心一致 (lx) - - # 侧板的 Y 偏移量 - # 放在斜坡宽度的两侧:(斜坡宽/2) + (侧板厚/2) - y_shift = (width / 2.0) + (side_thickness / 2.0) - - # 生成左右两个侧板 - for sign in [-1, 1]: # -1:左侧, 1:右侧 - if add_baffle_height and sign == 1: continue - local_y = sign * y_shift - - # 将 (lx, local_y) 旋转 Yaw 角到全局 - sgx, sgy = rot2d(lx, local_y, yaw) - - side_pos = [ - position[0] + sgx, - position[1] + sgy, - position[2] + side_lz - ] - - # 侧板的旋转角度与斜坡完全一致,这样上边缘才会平行 - self.AddBox(side_pos, final_euler, [ramp_len, side_thickness, side_h]) - - def AddSlopeGroup(self, position=[0.0, 0.0, 0.0], yaw=0.0, add_baffle=True, add_baffle_height=False,): - - L = 0.6 # 坡长 (爬升方向) - W = 0.6 # 坡宽 (侧向) - H = 0.164 - - p1_local = [-L / 2, -W] - yaw1 = np.pi / 2 - - p2_local = [-L, W/2] - yaw2 = 0 - - p3_local = [L, -W/2] - yaw3 = np.pi - - p4_local = [L / 2, W] - yaw4 = np.pi * 3 / 2 - - blocks = [ - (p1_local, yaw1), - (p2_local, yaw2), - (p3_local, yaw3), - (p4_local, yaw4) - ] - - for pos_local, local_yaw in blocks: - off_x, off_y = rot2d(pos_local[0], pos_local[1], yaw) - - abs_pos = [ - position[0] + off_x, - position[1] + off_y, - position[2] - ] - - abs_yaw = yaw + local_yaw - - self.AddSlope(position=abs_pos, - yaw=abs_yaw, - length=L, - width=W, - height=H, - add_baffle=add_baffle, - add_baffle_height=add_baffle_height) - - def AddBlockyHeightField( - self, - position=[1.0, 0.0, 0.0], - euler=[0.0, -0.0, 0.0], - size=[1.0, 1.0], - height_scale=0.2, # 高度差幅度 - negative_height=0.1, - image_width=128, - img_height=128, - smooth=50.0, # 注意:如果要完全随机,把这个数改得很小(如 2.0) - pixels_per_block=16, # <--- 新参数:决定方块的大小 - output_hfield_image="height_field.png"): - - # 1. 准备图像数据 - terrain_image = np.zeros((img_height, image_width), dtype=np.uint8) - - # 2. 预先生成一个随机种子偏移,保证每次地形不一样 - seed_offset_x = np.random.randint(0, 10000) - seed_offset_y = np.random.randint(0, 10000) - - for y in range(img_height): - for x in range(image_width): - - # === 核心修改开始 === - # 这里的整除逻辑 (//) 是制造“方块感”的关键 - # 它将坐标强制归整,例如 x=0到15 都会变成 0,x=16到31 都会变成 16 - # 这样这 16 个像素取到的噪声值就是一模一样的,形成平坦的台阶 - block_x = (x // pixels_per_block) * pixels_per_block - block_y = (y // pixels_per_block) * pixels_per_block - - # 使用归整后的 block_x, block_y 来生成噪声 - noise_value = noise.pnoise2((block_x + seed_offset_x) / smooth, - (block_y + seed_offset_y) / smooth, - octaves=1, # 减少细节,让方块表面平整 - persistence=0.5, - lacunarity=2.0) - # === 核心修改结束 === - - # 映射到 0-255 - terrain_image[y, x] = int((noise_value + 1) / 2 * 255) - - hfield = xml_et.SubElement(self.asset, "hfield") - hfield.attrib["name"] = "perlin_hfield" - hfield.attrib["size"] = list_to_str( - [size[0] / 2.0, size[1] / 2.0, height_scale, negative_height]) - hfield.attrib["file"] = output_hfield_image - - geo = xml_et.SubElement(self.worldbody, "geom") - geo.attrib["type"] = "hfield" - geo.attrib["hfield"] = "perlin_hfield" - geo.attrib["pos"] = list_to_str(position) - quat = euler_to_quat(euler[0], euler[1], euler[2]) - geo.attrib["quat"] = list_to_str(quat) - - def unit1_sponge(self): - self.AddSlope(position=[0.0, 1.2, 0.0], yaw=np.pi, length=2.32, width=1.2, height=0.6, add_baffle_height=True) - self.AddSlope(position=[-2.32, 0.0, 0.0], yaw=0, length=2.32, width=1.2, height=0.6, add_baffle_height=True) - - self.AddBox(position=[0.6, -0.6, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0]) - self.AddBox(position=[-2.92, -0.6, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0]) - self.AddBox(position=[0.6, 1.8, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0]) - self.AddBox(position=[-2.92, 1.8, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0]) - self.AddBox(position=[-3.52, 0.0, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0]) - - self.AddBox(position=[-2.92, 0.6, 0.025], size=[1.2, 2.4, 0.05], sponge=True) - self.AddBox(position=[0.6, 0.6, 0.025], size=[1.2, 2.4, 0.05], sponge=True) - - self.AddSlope(position=[0.0, 1.2, 0.05], yaw=np.pi, length=2.32, width=1.2, thickness=0.05, height=0.6, sponge=True, add_baffle=False) - self.AddSlope(position=[-2.32, 0.0, 0.05], yaw=0, length=2.32, width=1.2, thickness=0.05, height=0.6, sponge=True, add_baffle=False) - - - - def unit2_slopes(self): - self.AddBox(position=[1.225, 0.6, 0.0], size=[2.4, 0.05, 0.1], euler=[0, 0, np.pi / 2]) - self.AddSlopeGroup(position=[1.85, 0.0, 0.0], yaw=0.0, add_baffle=True) - self.AddSlopeGroup(position=[1.85, 1.2, 0.0], yaw=0.0, add_baffle=True) - self.AddBox(position=[2.475, 0.6, 0.0], size=[2.4, 0.05, 0.1], euler=[0, 0, np.pi / 2]) - self.AddSlopeGroup(position=[3.1, 0.0, 0.0], yaw=0.0, add_baffle=True) - self.AddSlopeGroup(position=[3.1, 1.2, 0.0], yaw=0.0, add_baffle=True) - self.AddSlopeGroup(position=[4.3, 0.0, 0.0], yaw=0.0, add_baffle=True) - self.AddSlopeGroup(position=[4.3, 1.2, 0.0], yaw=0.0, add_baffle=True) - self.AddBox(position=[3.7, 0.575, 0.0], size=[2.4, 0.05, 0.1]) - self.AddBox(position=[3.7, 0.625, 0.0], size=[2.4, 0.05, 0.1]) - self.AddBox(position=[4.925, 0.6, 0.0], size=[2.4, 0.05, 0.1], euler=[0, 0, np.pi / 2]) - self.AddSlopeGroup(position=[5.55, 0.0, 0.0], yaw=0.0, add_baffle=True) - self.AddSlopeGroup(position=[5.55, 1.2, 0.0], yaw=0.0, add_baffle=True) - self.AddBox(position=[6.175, 0.6, 0.0], size=[2.4, 0.05, 0.1], euler=[0, 0, np.pi / 2]) - self.AddBox(position=[1.9, 1.8, 0.3], size=[1.4, 0.6, 0.03], euler=[np.pi / 2, 0, 0]) - self.AddBox(position=[3.2, 1.8, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0]) - self.AddBox(position=[4.4, 1.8, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0]) - self.AddBox(position=[5.6, 1.8, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0]) - - self.AddBox(position=[1.9, -0.6, 0.3], size=[1.4, 0.6, 0.03], euler=[np.pi / 2, 0, 0]) - self.AddBox(position=[3.2, -0.6, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0]) - self.AddBox(position=[4.4, -0.6, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0]) - self.AddBox(position=[5.6, -0.6, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0]) - - self.AddBox(position=[1.2, 0.0, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0]) - self.AddBox(position=[2.5, 1.2, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0]) - self.AddBox(position=[4.95, 0.0, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0]) - self.AddBox(position=[6.2, 1.2, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0]) - - - def unit3_stairs(self): - self.AddBox(position = [6.8, 1.2, 0.1], size = [1.2, 1.2, 0.2]) - self.AddBox(position = [8.0, 0.0, 0.1], size = [1.2, 1.2, 0.2]) - self.AddBox(position = [9.2, 1.2, 0.1], size = [1.2, 1.2, 0.2]) - self.AddBox(position = [10.4, 0.0, 0.1], size = [1.2, 1.2, 0.2]) - - self.AddBox(position=[6.8, 1.8, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0]) - self.AddBox(position=[8.0, 1.8, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0]) - self.AddBox(position=[9.2, 1.8, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0]) - self.AddBox(position=[10.4, 1.8, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0]) - - self.AddBox(position=[6.8, -0.6, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0]) - self.AddBox(position=[8.0, -0.6, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0]) - self.AddBox(position=[9.2, -0.6, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0]) - self.AddBox(position=[10.4, -0.6, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0]) - - self.AddBox(position=[11.0, 1.2, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0]) - - def unit4_diagonal(self): - self.AddSlope(position=[12.8, 1.8, 0.0], yaw=np.pi / 2, length=2.32, width=1.2, height=0.6, add_baffle_height=True) - self.AddSlope(position=[12.8, 1.8, 0.05], yaw=np.pi / 2, length=2.32/48, width=1.2, height=0.6/48, thickness=0.05, add_baffle=False) - self.AddSlope(position=[12.8, 4.07, 0.6375], yaw=np.pi / 2, length=2.32/48, width=1.2, height=0.6/48, thickness=0.05, add_baffle=False) - self.AddSlope(position=[12.225, 1.8, 0.05], yaw=np.pi / 2, length=2.32, width=0.05, height=0.6, thickness=0.05, add_baffle=False) - self.AddSlope(position=[13.375, 1.8, 0.05], yaw=np.pi / 2, length=2.32, width=0.05, height=0.6, thickness=0.05, add_baffle=False) - self.AddBox(position=[12.8, 3.54, 0.475], size=[1.64, 0.05, 0.05], euler=[0.0, -0.1779, 0.7685]) - self.AddBox(position=[12.8, 2.38, 0.175], size=[1.64, 0.05, 0.05], euler=[0.0, 0.1779, -0.7685]) - - self.AddSlope(position=[11.6, 4.12, 0.0], yaw=-np.pi / 2, length=2.32, width=1.2, height=0.6, add_baffle_height=True) - self.AddSlope(position=[11.6, 4.12, 0.05], yaw=-np.pi / 2, length=2.32/48, width=1.2, height=0.6/48, thickness=0.05, add_baffle=False) - self.AddSlope(position=[11.6, 1.85, 0.6375], yaw=-np.pi / 2, length=2.32/48, width=1.2, height=0.6/48, thickness=0.05, add_baffle=False) - self.AddSlope(position=[12.175, 4.12, 0.05], yaw=-np.pi / 2, length=2.32, width=0.05, height=0.6, thickness=0.05, add_baffle=False) - self.AddSlope(position=[11.025, 4.12, 0.05], yaw=-np.pi / 2, length=2.32, width=0.05, height=0.6, thickness=0.05, add_baffle=False) - self.AddBox(position=[11.6, 3.54, 0.175], size=[1.64, 0.05, 0.05], euler=[0.0, -0.1779, -0.7685]) - self.AddBox(position=[11.6, 2.38, 0.475], size=[1.64, 0.05, 0.05], euler=[0.0, 0.1779, 0.7685]) - - self.AddBox(position=[11.6, -0.6, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0]) - self.AddBox(position=[12.2, 0.0, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0]) - self.AddBox(position=[12.8, 0.6, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0]) - self.AddBox(position=[13.4, 1.2, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0]) - - self.AddBox(position=[12.2, 0.625, 0.025], size=[2.4, 0.05, 0.05]) - self.AddBox(position=[12.2, 1.775, 0.025], size=[2.4, 0.05, 0.05]) - self.AddBox(position=[11.025, 1.2, 0.025], size=[1.2, 0.05, 0.05], euler=[0, 0, np.pi / 2]) - self.AddBox(position=[13.375, 1.2, 0.025], size=[1.2, 0.05, 0.05], euler=[0, 0, np.pi / 2]) - self.AddBox(position=[11.6, 1.2, 0.025], size=[1.64, 0.05, 0.05], euler=[0, 0, -np.pi / 4]) - self.AddBox(position=[12.8, 1.2, 0.025], size=[1.64, 0.05, 0.05], euler=[0, 0, np.pi / 4]) - - self.AddBox(position=[12.2, 4.145, 0.025], size=[2.4, 0.05, 0.05]) - self.AddBox(position=[12.2, 5.295, 0.025], size=[2.4, 0.05, 0.05]) - self.AddBox(position=[11.025, 4.72, 0.025], size=[1.2, 0.05, 0.05], euler=[0, 0, np.pi / 2]) - self.AddBox(position=[13.375, 4.72, 0.025], size=[1.2, 0.05, 0.05], euler=[0, 0, np.pi / 2]) - self.AddBox(position=[11.6, 4.72, 0.025], size=[1.64, 0.05, 0.05], euler=[0, 0, np.pi / 4]) - self.AddBox(position=[12.8, 4.72, 0.025], size=[1.64, 0.05, 0.05], euler=[0, 0, -np.pi / 4]) - - self.AddBox(position=[11.0, 4.72, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0]) - self.AddBox(position=[13.4, 4.72, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0]) - self.AddBox(position=[11.6, 5.32, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0]) - - self.AddBox(position=[11.6, 0.0, 0.0], size=[1.2, 1.2, 0.03]) - self.AddBox(position=[12.2, 1.2, 0.0], size=[2.4, 1.2, 0.03]) - self.AddBox(position=[12.2, 4.72, 0.0], size=[2.4, 1.2, 0.03]) - - - def unit5_sandstone(self): - - self.AddBox(position=[11.0, 5.92, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0]) - self.AddBox(position=[11.0, 7.12, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0]) - self.AddBox(position=[11.0, 8.32, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0]) - self.AddBox(position=[11.0, 9.52, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0]) - self.AddBox(position=[13.4, 5.92, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0]) - self.AddBox(position=[13.4, 7.12, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0]) - self.AddBox(position=[13.4, 8.32, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0]) - self.AddBox(position=[13.4, 9.52, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0]) - - self.AddBox(position=[12.8, 6.52, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0]) - self.AddBox(position=[12.8, 10.12, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0]) - self.AddBox(position=[11.6, 8.92, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0]) - - self.AddBox(position=[12.2, 6.545, 0.05], size=[2.4, 0.05, 0.05]) - self.AddBox(position=[12.2, 8.895, 0.05], size=[2.4, 0.05, 0.05]) - self.AddBox(position=[11.025, 7.72, 0.05], size=[2.4, 0.05, 0.05], euler=[0, 0, np.pi / 2]) - self.AddBox(position=[13.375, 7.72, 0.05], size=[2.4, 0.05, 0.05], euler=[0, 0, np.pi / 2]) - self.AddBox(position=[12.2, 7.72, 0.05], size=[3.28, 0.05, 0.05], euler=[0, 0, -np.pi / 4]) - self.AddBox(position=[12.2, 7.72, 0.05], size=[3.28, 0.05, 0.05], euler=[0, 0, np.pi / 4]) - self.AddBox(position=[11.6, 7.12, 0.05], size=[1.64, 0.05, 0.05], euler=[0, 0, -np.pi / 4]) - self.AddBox(position=[12.8, 7.12, 0.05], size=[1.64, 0.05, 0.05], euler=[0, 0, np.pi / 4]) - self.AddBox(position=[11.6, 8.32, 0.05], size=[1.64, 0.05, 0.05], euler=[0, 0, np.pi / 4]) - self.AddBox(position=[12.8, 8.32, 0.05], size=[1.64, 0.05, 0.05], euler=[0, 0, -np.pi / 4]) - - self.AddBlockyHeightField(position=[12.2, 7.72, -0.00], size=[2.4, 4.8], height_scale=0.08) - - -if __name__ == "__main__": - tg = TerrainGenerator() - - # # Box obstacle - # tg.AddBox(position=[0.55 / 2 + 2, 0, 0.075], size=[np.sqrt(0.55 * 0.55 + 0.15*0.15), 0.1, 0.01], euler=[np.pi / 2, 0, 0]) - # tg.AddBox(position=[3.5, 0, 0.075], size=[1.5, 0.5, 0.01], euler=[0, 0, 0]) - - # # Geometry obstacle - # # geo_type supports "plane", "sphere", "capsule", "ellipsoid", "cylinder", "box" - # tg.AddGeometry(position=[1.5, 0.0, 0.25], euler=[0, 0, 0.0], size=[1.0,0.5,0.5],geo_type="cylinder") - - # # Slope - # tg.AddBox(position=[2.0, 2.0, 0.5], - # euler=[0.0, -0.5, 0.0], - # size=[3, 1.5, 0.1]) - - # # Stairs - # tg.AddStairs(init_pos=[1.0, 4.0, 0.0], yaw=0.0) - - # # Suspend stairs - # tg.AddSuspendStairs(init_pos=[1.0, 6.0, 0.0], yaw=0.0) - - # # Rough ground - # tg.AddRoughGround(init_pos=[-2.5, 5.0, 0.0], - # euler=[0, 0, 0.0], - # nums=[10, 8]) - - # # Perlin heigh field - # tg.AddPerlinHeighField(position=[-1.5, 4.0, 0.0], size=[2.0, 1.5]) - - # # Heigh field from image - # tg.AddHeighFieldFromImage(position=[-1.5, 2.0, 0.0], - # euler=[0, 0, -1.57], - # size=[2.0,2.0], - # input_img="./unitree_robot.jpeg", - # image_scale=[1.0, 1.0], - # output_hfield_image="unitree_hfield.png") - tg.unit1_sponge() - tg.unit2_slopes() - tg.unit3_stairs() - tg.unit4_diagonal() - tg.unit5_sandstone() - - tg.Save() diff --git a/resources/robots/go2/stairs.xml b/resources/robots/go2/stairs.xml index 80d0cd0..6fb6c1a 100644 --- a/resources/robots/go2/stairs.xml +++ b/resources/robots/go2/stairs.xml @@ -1,5 +1,5 @@ - +