Source code for isaaclab.terrains.height_field.hf_terrains
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.# All rights reserved.## SPDX-License-Identifier: BSD-3-Clause"""Functions to generate height fields for different terrains."""from__future__importannotationsimportnumpyasnpimportscipy.interpolateasinterpolatefromtypingimportTYPE_CHECKINGfrom.utilsimportheight_field_to_meshifTYPE_CHECKING:from.importhf_terrains_cfg
[docs]@height_field_to_meshdefrandom_uniform_terrain(difficulty:float,cfg:hf_terrains_cfg.HfRandomUniformTerrainCfg)->np.ndarray:"""Generate a terrain with height sampled uniformly from a specified range. .. image:: ../../_static/terrains/height_field/random_uniform_terrain.jpg :width: 40% :align: center Note: The :obj:`difficulty` parameter is ignored for this terrain. Args: difficulty: The difficulty of the terrain. This is a value between 0 and 1. cfg: The configuration for the terrain. Returns: The height field of the terrain as a 2D numpy array with discretized heights. The shape of the array is (width, length), where width and length are the number of points along the x and y axis, respectively. Raises: ValueError: When the downsampled scale is smaller than the horizontal scale. """# check parameters# -- horizontal scaleifcfg.downsampled_scaleisNone:cfg.downsampled_scale=cfg.horizontal_scaleelifcfg.downsampled_scale<cfg.horizontal_scale:raiseValueError("Downsampled scale must be larger than or equal to the horizontal scale:"f" {cfg.downsampled_scale} < {cfg.horizontal_scale}.")# switch parameters to discrete units# -- horizontal scalewidth_pixels=int(cfg.size[0]/cfg.horizontal_scale)length_pixels=int(cfg.size[1]/cfg.horizontal_scale)# -- downsampled scalewidth_downsampled=int(cfg.size[0]/cfg.downsampled_scale)length_downsampled=int(cfg.size[1]/cfg.downsampled_scale)# -- heightheight_min=int(cfg.noise_range[0]/cfg.vertical_scale)height_max=int(cfg.noise_range[1]/cfg.vertical_scale)height_step=int(cfg.noise_step/cfg.vertical_scale)# create range of heights possibleheight_range=np.arange(height_min,height_max+height_step,height_step)# sample heights randomly from the range along a gridheight_field_downsampled=np.random.choice(height_range,size=(width_downsampled,length_downsampled))# create interpolation function for the sampled heightsx=np.linspace(0,cfg.size[0]*cfg.horizontal_scale,width_downsampled)y=np.linspace(0,cfg.size[1]*cfg.horizontal_scale,length_downsampled)func=interpolate.RectBivariateSpline(x,y,height_field_downsampled)# interpolate the sampled heights to obtain the height fieldx_upsampled=np.linspace(0,cfg.size[0]*cfg.horizontal_scale,width_pixels)y_upsampled=np.linspace(0,cfg.size[1]*cfg.horizontal_scale,length_pixels)z_upsampled=func(x_upsampled,y_upsampled)# round off the interpolated heights to the nearest vertical stepreturnnp.rint(z_upsampled).astype(np.int16)
[docs]@height_field_to_meshdefpyramid_sloped_terrain(difficulty:float,cfg:hf_terrains_cfg.HfPyramidSlopedTerrainCfg)->np.ndarray:"""Generate a terrain with a truncated pyramid structure. The terrain is a pyramid-shaped sloped surface with a slope of :obj:`slope` that trims into a flat platform at the center. The slope is defined as the ratio of the height change along the x axis to the width along the x axis. For example, a slope of 1.0 means that the height changes by 1 unit for every 1 unit of width. If the :obj:`cfg.inverted` flag is set to :obj:`True`, the terrain is inverted such that the platform is at the bottom. .. image:: ../../_static/terrains/height_field/pyramid_sloped_terrain.jpg :width: 40% .. image:: ../../_static/terrains/height_field/inverted_pyramid_sloped_terrain.jpg :width: 40% Args: difficulty: The difficulty of the terrain. This is a value between 0 and 1. cfg: The configuration for the terrain. Returns: The height field of the terrain as a 2D numpy array with discretized heights. The shape of the array is (width, length), where width and length are the number of points along the x and y axis, respectively. """# resolve terrain configurationifcfg.inverted:slope=-cfg.slope_range[0]-difficulty*(cfg.slope_range[1]-cfg.slope_range[0])else:slope=cfg.slope_range[0]+difficulty*(cfg.slope_range[1]-cfg.slope_range[0])# switch parameters to discrete units# -- horizontal scalewidth_pixels=int(cfg.size[0]/cfg.horizontal_scale)length_pixels=int(cfg.size[1]/cfg.horizontal_scale)# -- height# we want the height to be 1/2 of the width since the terrain is a pyramidheight_max=int(slope*cfg.size[0]/2/cfg.vertical_scale)# -- center of the terraincenter_x=int(width_pixels/2)center_y=int(length_pixels/2)# create a meshgrid of the terrainx=np.arange(0,width_pixels)y=np.arange(0,length_pixels)xx,yy=np.meshgrid(x,y,sparse=True)# offset the meshgrid to the center of the terrainxx=(center_x-np.abs(center_x-xx))/center_xyy=(center_y-np.abs(center_y-yy))/center_y# reshape the meshgrid to be 2Dxx=xx.reshape(width_pixels,1)yy=yy.reshape(1,length_pixels)# create a sloped surfacehf_raw=np.zeros((width_pixels,length_pixels))hf_raw=height_max*xx*yy# create a flat platform at the center of the terrainplatform_width=int(cfg.platform_width/cfg.horizontal_scale/2)# get the height of the platform at the corner of the platformx_pf=width_pixels//2-platform_widthy_pf=length_pixels//2-platform_widthz_pf=hf_raw[x_pf,y_pf]hf_raw=np.clip(hf_raw,min(0,z_pf),max(0,z_pf))# round off the heights to the nearest vertical stepreturnnp.rint(hf_raw).astype(np.int16)
[docs]@height_field_to_meshdefpyramid_stairs_terrain(difficulty:float,cfg:hf_terrains_cfg.HfPyramidStairsTerrainCfg)->np.ndarray:"""Generate a terrain with a pyramid stair pattern. The terrain is a pyramid stair pattern which trims to a flat platform at the center of the terrain. If the :obj:`cfg.inverted` flag is set to :obj:`True`, the terrain is inverted such that the platform is at the bottom. .. image:: ../../_static/terrains/height_field/pyramid_stairs_terrain.jpg :width: 40% .. image:: ../../_static/terrains/height_field/inverted_pyramid_stairs_terrain.jpg :width: 40% Args: difficulty: The difficulty of the terrain. This is a value between 0 and 1. cfg: The configuration for the terrain. Returns: The height field of the terrain as a 2D numpy array with discretized heights. The shape of the array is (width, length), where width and length are the number of points along the x and y axis, respectively. """# resolve terrain configurationstep_height=cfg.step_height_range[0]+difficulty*(cfg.step_height_range[1]-cfg.step_height_range[0])ifcfg.inverted:step_height*=-1# switch parameters to discrete units# -- terrainwidth_pixels=int(cfg.size[0]/cfg.horizontal_scale)length_pixels=int(cfg.size[1]/cfg.horizontal_scale)# -- stairsstep_width=int(cfg.step_width/cfg.horizontal_scale)step_height=int(step_height/cfg.vertical_scale)# -- platformplatform_width=int(cfg.platform_width/cfg.horizontal_scale)# create a terrain with a flat platform at the centerhf_raw=np.zeros((width_pixels,length_pixels))# add the stepscurrent_step_height=0start_x,start_y=0,0stop_x,stop_y=width_pixels,length_pixelswhile(stop_x-start_x)>platform_widthand(stop_y-start_y)>platform_width:# increment position# -- xstart_x+=step_widthstop_x-=step_width# -- ystart_y+=step_widthstop_y-=step_width# increment heightcurrent_step_height+=step_height# add the stephf_raw[start_x:stop_x,start_y:stop_y]=current_step_height# round off the heights to the nearest vertical stepreturnnp.rint(hf_raw).astype(np.int16)
[docs]@height_field_to_meshdefdiscrete_obstacles_terrain(difficulty:float,cfg:hf_terrains_cfg.HfDiscreteObstaclesTerrainCfg)->np.ndarray:"""Generate a terrain with randomly generated obstacles as pillars with positive and negative heights. The terrain is a flat platform at the center of the terrain with randomly generated obstacles as pillars with positive and negative height. The obstacles are randomly generated cuboids with a random width and height. They are placed randomly on the terrain with a minimum distance of :obj:`cfg.platform_width` from the center of the terrain. .. image:: ../../_static/terrains/height_field/discrete_obstacles_terrain.jpg :width: 40% :align: center Args: difficulty: The difficulty of the terrain. This is a value between 0 and 1. cfg: The configuration for the terrain. Returns: The height field of the terrain as a 2D numpy array with discretized heights. The shape of the array is (width, length), where width and length are the number of points along the x and y axis, respectively. """# resolve terrain configurationobs_height=cfg.obstacle_height_range[0]+difficulty*(cfg.obstacle_height_range[1]-cfg.obstacle_height_range[0])# switch parameters to discrete units# -- terrainwidth_pixels=int(cfg.size[0]/cfg.horizontal_scale)length_pixels=int(cfg.size[1]/cfg.horizontal_scale)# -- obstaclesobs_height=int(obs_height/cfg.vertical_scale)obs_width_min=int(cfg.obstacle_width_range[0]/cfg.horizontal_scale)obs_width_max=int(cfg.obstacle_width_range[1]/cfg.horizontal_scale)# -- center of the terrainplatform_width=int(cfg.platform_width/cfg.horizontal_scale)# create discrete ranges for the obstacles# -- shapeobs_width_range=np.arange(obs_width_min,obs_width_max,4)obs_length_range=np.arange(obs_width_min,obs_width_max,4)# -- positionobs_x_range=np.arange(0,width_pixels,4)obs_y_range=np.arange(0,length_pixels,4)# create a terrain with a flat platform at the centerhf_raw=np.zeros((width_pixels,length_pixels))# generate the obstaclesfor_inrange(cfg.num_obstacles):# sample sizeifcfg.obstacle_height_mode=="choice":height=np.random.choice([-obs_height,-obs_height//2,obs_height//2,obs_height])elifcfg.obstacle_height_mode=="fixed":height=obs_heightelse:raiseValueError(f"Unknown obstacle height mode '{cfg.obstacle_height_mode}'. Must be 'choice' or 'fixed'.")width=int(np.random.choice(obs_width_range))length=int(np.random.choice(obs_length_range))# sample positionx_start=int(np.random.choice(obs_x_range))y_start=int(np.random.choice(obs_y_range))# clip start position to the terrainifx_start+width>width_pixels:x_start=width_pixels-widthify_start+length>length_pixels:y_start=length_pixels-length# add to terrainhf_raw[x_start:x_start+width,y_start:y_start+length]=height# clip the terrain to the platformx1=(width_pixels-platform_width)//2x2=(width_pixels+platform_width)//2y1=(length_pixels-platform_width)//2y2=(length_pixels+platform_width)//2hf_raw[x1:x2,y1:y2]=0# round off the heights to the nearest vertical stepreturnnp.rint(hf_raw).astype(np.int16)
[docs]@height_field_to_meshdefwave_terrain(difficulty:float,cfg:hf_terrains_cfg.HfWaveTerrainCfg)->np.ndarray:r"""Generate a terrain with a wave pattern. The terrain is a flat platform at the center of the terrain with a wave pattern. The wave pattern is generated by adding sinusoidal waves based on the number of waves and the amplitude of the waves. The height of the terrain at a point :math:`(x, y)` is given by: .. math:: h(x, y) = A \left(\sin\left(\frac{2 \pi x}{\lambda}\right) + \cos\left(\frac{2 \pi y}{\lambda}\right) \right) where :math:`A` is the amplitude of the waves, :math:`\lambda` is the wavelength of the waves. .. image:: ../../_static/terrains/height_field/wave_terrain.jpg :width: 40% :align: center Args: difficulty: The difficulty of the terrain. This is a value between 0 and 1. cfg: The configuration for the terrain. Returns: The height field of the terrain as a 2D numpy array with discretized heights. The shape of the array is (width, length), where width and length are the number of points along the x and y axis, respectively. Raises: ValueError: When the number of waves is non-positive. """# check number of wavesifcfg.num_waves<0:raiseValueError(f"Number of waves must be a positive integer. Got: {cfg.num_waves}.")# resolve terrain configurationamplitude=cfg.amplitude_range[0]+difficulty*(cfg.amplitude_range[1]-cfg.amplitude_range[0])# switch parameters to discrete units# -- terrainwidth_pixels=int(cfg.size[0]/cfg.horizontal_scale)length_pixels=int(cfg.size[1]/cfg.horizontal_scale)amplitude_pixels=int(0.5*amplitude/cfg.vertical_scale)# compute the wave number: nu = 2 * pi / lambdawave_length=length_pixels/cfg.num_waveswave_number=2*np.pi/wave_length# create meshgrid for the terrainx=np.arange(0,width_pixels)y=np.arange(0,length_pixels)xx,yy=np.meshgrid(x,y,sparse=True)xx=xx.reshape(width_pixels,1)yy=yy.reshape(1,length_pixels)# create a terrain with a flat platform at the centerhf_raw=np.zeros((width_pixels,length_pixels))# add the waveshf_raw+=amplitude_pixels*(np.cos(yy*wave_number)+np.sin(xx*wave_number))# round off the heights to the nearest vertical stepreturnnp.rint(hf_raw).astype(np.int16)
[docs]@height_field_to_meshdefstepping_stones_terrain(difficulty:float,cfg:hf_terrains_cfg.HfSteppingStonesTerrainCfg)->np.ndarray:"""Generate a terrain with a stepping stones pattern. The terrain is a stepping stones pattern which trims to a flat platform at the center of the terrain. .. image:: ../../_static/terrains/height_field/stepping_stones_terrain.jpg :width: 40% :align: center Args: difficulty: The difficulty of the terrain. This is a value between 0 and 1. cfg: The configuration for the terrain. Returns: The height field of the terrain as a 2D numpy array with discretized heights. The shape of the array is (width, length), where width and length are the number of points along the x and y axis, respectively. """# resolve terrain configurationstone_width=cfg.stone_width_range[1]-difficulty*(cfg.stone_width_range[1]-cfg.stone_width_range[0])stone_distance=cfg.stone_distance_range[0]+difficulty*(cfg.stone_distance_range[1]-cfg.stone_distance_range[0])# switch parameters to discrete units# -- terrainwidth_pixels=int(cfg.size[0]/cfg.horizontal_scale)length_pixels=int(cfg.size[1]/cfg.horizontal_scale)# -- stonesstone_distance=int(stone_distance/cfg.horizontal_scale)stone_width=int(stone_width/cfg.horizontal_scale)stone_height_max=int(cfg.stone_height_max/cfg.vertical_scale)# -- holesholes_depth=int(cfg.holes_depth/cfg.vertical_scale)# -- platformplatform_width=int(cfg.platform_width/cfg.horizontal_scale)# create range of heightsstone_height_range=np.arange(-stone_height_max-1,stone_height_max,step=1)# create a terrain with a flat platform at the centerhf_raw=np.full((width_pixels,length_pixels),holes_depth)# add the stonesstart_x,start_y=0,0# -- if the terrain is longer than it is wide then fill the terrain column by columniflength_pixels>=width_pixels:whilestart_y<length_pixels:# ensure that stone stops along y-axisstop_y=min(length_pixels,start_y+stone_width)# randomly sample x-positionstart_x=np.random.randint(0,stone_width)stop_x=max(0,start_x-stone_distance)# fill first stonehf_raw[0:stop_x,start_y:stop_y]=np.random.choice(stone_height_range)# fill row with stoneswhilestart_x<width_pixels:stop_x=min(width_pixels,start_x+stone_width)hf_raw[start_x:stop_x,start_y:stop_y]=np.random.choice(stone_height_range)start_x+=stone_width+stone_distance# update y-positionstart_y+=stone_width+stone_distanceelifwidth_pixels>length_pixels:whilestart_x<width_pixels:# ensure that stone stops along x-axisstop_x=min(width_pixels,start_x+stone_width)# randomly sample y-positionstart_y=np.random.randint(0,stone_width)stop_y=max(0,start_y-stone_distance)# fill first stonehf_raw[start_x:stop_x,0:stop_y]=np.random.choice(stone_height_range)# fill column with stoneswhilestart_y<length_pixels:stop_y=min(length_pixels,start_y+stone_width)hf_raw[start_x:stop_x,start_y:stop_y]=np.random.choice(stone_height_range)start_y+=stone_width+stone_distance# update x-positionstart_x+=stone_width+stone_distance# add the platform in the centerx1=(width_pixels-platform_width)//2x2=(width_pixels+platform_width)//2y1=(length_pixels-platform_width)//2y2=(length_pixels+platform_width)//2hf_raw[x1:x2,y1:y2]=0# round off the heights to the nearest vertical stepreturnnp.rint(hf_raw).astype(np.int16)