@@ -51,6 +51,27 @@ def _get_texture(name):
5151_DEFAULT_PITCH_SIZE = (12 , 9 )
5252_DEFAULT_GOAL_LENGTH_RATIO = 0.33 # Goal length / pitch width.
5353
54+ _GOALPOST_RELATIVE_SIZE = 0.07 # Ratio of the goalpost radius to goal size.
55+ _NET_RELATIVE_SIZE = 0.01 # Ratio of the net thickness to goal size.
56+ _SUPPORT_POST_RATIO = 0.75 # Ratio of support post to goalpost radius.
57+ # Goalposts defined in the unit box [-1, 1]**3 facing to the positive X.
58+ _GOALPOSTS = {'right_post' : (1 , - 1 , - 1 , 1 , - 1 , 1 ),
59+ 'left_post' : (1 , 1 , - 1 , 1 , 1 , 1 ),
60+ 'top_post' : (1 , - 1 , 1 , 1 , 1 , 1 ),
61+ 'right_base' : (1 , - 1 , - 1 , - 1 , - 1 , - 1 ),
62+ 'left_base' : (1 , 1 , - 1 , - 1 , 1 , - 1 ),
63+ 'back_base' : (- 1 , - 1 , - 1 , - 1 , 1 , - 1 ),
64+ 'right_support' : (- 1 , - 1 , - 1 , .2 , - 1 , 1 ),
65+ 'right_top_support' : (.2 , - 1 , 1 , 1 , - 1 , 1 ),
66+ 'left_support' : (- 1 , 1 , - 1 , .2 , 1 , 1 ),
67+ 'left_top_support' : (.2 , 1 , 1 , 1 , 1 , 1 )}
68+ # Vertices of net polygons, reshaped to 4x3 arrays.
69+ _NET = {'top' : _GOALPOSTS ['right_top_support' ] + _GOALPOSTS ['left_top_support' ],
70+ 'back' : _GOALPOSTS ['right_support' ] + _GOALPOSTS ['left_support' ],
71+ 'left' : _GOALPOSTS ['left_base' ] + _GOALPOSTS ['left_top_support' ],
72+ 'right' : _GOALPOSTS ['right_base' ] + _GOALPOSTS ['right_top_support' ]}
73+ _NET = {key : np .array (value ).reshape (4 , 3 ) for key , value in _NET .items ()}
74+
5475
5576def _top_down_cam_fovy (size , top_camera_distance ):
5677 return (360 / np .pi ) * np .arctan2 (_TOP_CAMERA_Y_PADDING_FACTOR * max (size ),
@@ -84,6 +105,166 @@ def _roof_size(size):
84105 return (size [0 ], size [1 ], _WALL_THICKNESS )
85106
86107
108+ def _goalpost_radius (size ):
109+ """Compute goal post radius as scaled average goal size."""
110+ return _GOALPOST_RELATIVE_SIZE * sum (size ) / 3.
111+
112+
113+ def _post_radius (goalpost_name , goalpost_radius ):
114+ """Compute the radius of a specific goalpost."""
115+ radius = goalpost_radius
116+ if 'top' in goalpost_name :
117+ radius *= 1.01 # Prevent z-fighting at the corners.
118+ if 'support' in goalpost_name :
119+ radius *= _SUPPORT_POST_RATIO # Suport posts are a bit narrower.
120+ return radius
121+
122+
123+ def _goalpost_fromto (unit_fromto , size , pos , direction ):
124+ """Rotate, scale and translate the `fromto` attribute of a goalpost.
125+
126+ The goalposts are defined in the unit cube [-1, 1]**3 using MuJoCo fromto
127+ specifier for capsules, they are then flipped according to whether they face
128+ in the +x or -x, scaled and moved.
129+
130+ Args:
131+ unit_fromto: two concatenated 3-vectors in the unit cube in xyzxyz order.
132+ size: a 3-vector, scaling of the goal.
133+ pos: a 3-vector, goal position.
134+ direction: a 3-vector, either (1,1,1) or (-1,01,1), direction of the goal
135+ along the x-axis.
136+
137+ Returns:
138+ two concatenated 3-vectors, the `fromto` of a goal geom.
139+ """
140+ fromto = np .array (unit_fromto ) * np .hstack ((direction , direction ))
141+ return fromto * np .array (size + size ) + np .array (pos + pos )
142+
143+
144+ class Goal (props .PositionDetector ):
145+ """Goal for soccer-like games: A PositionDetector with goalposts."""
146+
147+ def _make_net_vertices (self , size = (1 , 1 , 1 )):
148+ """Make vertices for the four net meshes by offsetting net polygons."""
149+ thickness = _NET_RELATIVE_SIZE * sum (size ) / 3
150+ # Get mesh offsets, compensate for mesh.scale deformation.
151+ dx = np .array ((thickness / size [0 ], 0 , 0 ))
152+ dy = np .array ((0 , thickness / size [1 ], 0 ))
153+ dz = np .array ((0 , 0 , thickness / size [2 ]))
154+ # Make mesh vertices with specified thickness.
155+ top = [v + dz for v in _NET ['top' ]] + [v - dz for v in _NET ['top' ]]
156+ right = [v + dy for v in _NET ['right' ]] + [v - dy for v in _NET ['right' ]]
157+ left = [v + dy for v in _NET ['left' ]] + [v - dy for v in _NET ['left' ]]
158+ back = ([v + dz for v in _NET ['back' ] if v [2 ] == 1 ] +
159+ [v - dz for v in _NET ['back' ] if v [2 ] == 1 ] +
160+ [v + dx for v in _NET ['back' ] if v [2 ] == - 1 ] +
161+ [v - dx for v in _NET ['back' ] if v [2 ] == - 1 ])
162+ vertices = {'top' : top , 'back' : back , 'left' : left , 'right' : right }
163+ return {key : (val * self ._direction ).flatten ()
164+ for key , val in vertices .items ()}
165+
166+ def _move_goal (self , pos , size ):
167+ """Translate and scale the goal."""
168+ for geom in self ._goal_geoms :
169+ unit_fromto = _GOALPOSTS [geom .name ]
170+ geom .fromto = _goalpost_fromto (unit_fromto , size , pos , self ._direction )
171+ geom .size = (_post_radius (geom .name , self ._goalpost_radius ),)
172+ if self ._make_net :
173+ net_vertices = self ._make_net_vertices (size )
174+ for geom in self ._net_geoms :
175+ geom .pos = pos
176+ geom .mesh .vertex = net_vertices [geom .mesh .name ]
177+ geom .mesh .scale = size
178+
179+ def _build (self , direction , net_rgba = (1 , 1 , 1 , .15 ), make_net = True , ** kwargs ):
180+ """Builds the goalposts and net.
181+
182+ Args:
183+ direction: Is the goal oriented towards positive or negative x-axis.
184+ net_rgba: rgba value of the net geoms.
185+ make_net: Where to add net geoms.
186+ **kwargs: arguments of PositionDetector superclass, see therein.
187+
188+ Raises:
189+ ValueError: If either `pos` or `size` arrays are not of length 3.
190+ ValueError: If direction in not 1 or -1.
191+ """
192+ if len (kwargs ['size' ]) != 3 or len (kwargs ['pos' ]) != 3 :
193+ raise ValueError ('Only 3D Goals are supported.' )
194+ if direction not in [1 , - 1 ]:
195+ raise ValueError ('direction must be either 1 or -1.' )
196+ # Flip both x and y, to maintain left / right name correctness.
197+ self ._direction = np .array ((direction , direction , 1 ))
198+ self ._make_net = make_net
199+
200+ # Force the underlying PositionDetector to a non visible site group.
201+ kwargs ['visible' ] = False
202+ # Make a Position_Detector.
203+ super (Goal , self )._build (** kwargs )
204+
205+ # Add goalpost geoms.
206+ size = kwargs ['size' ]
207+ pos = kwargs ['pos' ]
208+ self ._goalpost_radius = _goalpost_radius (size )
209+ self ._goal_geoms = []
210+ for geom_name , unit_fromto in _GOALPOSTS .items ():
211+ geom_fromto = _goalpost_fromto (unit_fromto , size , pos , self ._direction )
212+ geom_size = (_post_radius (geom_name , self ._goalpost_radius ),)
213+ self ._goal_geoms .append (
214+ self ._mjcf_root .worldbody .add (
215+ 'geom' ,
216+ type = 'capsule' ,
217+ name = geom_name ,
218+ size = geom_size ,
219+ fromto = geom_fromto ,
220+ rgba = self .goalpost_rgba ))
221+
222+ # Add net meshes and geoms.
223+ if self ._make_net :
224+ net_vertices = self ._make_net_vertices ()
225+ self ._net_geoms = []
226+ for name , vertex in net_vertices .items ():
227+ mesh = self ._mjcf_root .asset .add ('mesh' , name = name , vertex = vertex )
228+ geom = self ._mjcf_root .worldbody .add ('geom' , type = 'mesh' , mesh = mesh ,
229+ name = name , rgba = net_rgba ,
230+ contype = 0 , conaffinity = 0 )
231+ self ._net_geoms .append (geom )
232+
233+ def resize (self , pos , size ):
234+ """Call PositionDetector.resize(), move the goal."""
235+ super (Goal , self ).resize (pos , size )
236+ self ._goalpost_radius = _goalpost_radius (size )
237+ self ._move_goal (pos , size )
238+
239+ def set_position (self , physics , pos ):
240+ """Call PositionDetector.set_position(), move the goal."""
241+ super (Goal , self ).set_position (pos )
242+ size = 0.5 * (self .upper - self .lower )
243+ self ._move_goal (pos , size )
244+
245+ def _update_detection (self , physics ):
246+ """Call PositionDetector._update_detection(), then recolor the goalposts."""
247+ super (Goal , self )._update_detection (physics )
248+ if self ._detected and not self ._previously_detected :
249+ physics .bind (self ._goal_geoms ).rgba = self .goalpost_detected_rgba
250+ elif self ._previously_detected and not self ._detected :
251+ physics .bind (self ._goal_geoms ).rgba = self .goalpost_rgba
252+
253+ @property
254+ def goalpost_rgba (self ):
255+ """Goalposts are always opaque."""
256+ rgba = self ._rgba .copy ()
257+ rgba [3 ] = 1
258+ return rgba
259+
260+ @property
261+ def goalpost_detected_rgba (self ):
262+ """Goalposts are always opaque."""
263+ detected_rgba = self ._detected_rgba .copy ()
264+ detected_rgba [3 ] = 1
265+ return detected_rgba
266+
267+
87268class Pitch (composer .Arena ):
88269 """A pitch with a plane, two goals and a field with position detection."""
89270
@@ -153,6 +334,7 @@ def _build(self,
153334 'material' , name = 'groundplane' , texture = self ._ground_texture )
154335 self ._ground_geom = self ._mjcf_root .worldbody .add (
155336 'geom' ,
337+ name = 'ground' ,
156338 type = 'plane' ,
157339 material = self ._ground_material ,
158340 size = list (self ._size ) + [max (self ._size ) * _GROUND_GEOM_GRID_RATIO ])
@@ -174,19 +356,23 @@ def _build(self,
174356 # goal position detector before bouncing off the field_box.
175357 self ._fb_offset = 0.5 if field_box else 0.0
176358 goal_size = self ._get_goal_size ()
177- self ._home_goal = props .PositionDetector (
359+ self ._home_goal = Goal (
360+ direction = 1 ,
361+ make_net = False ,
178362 pos = (- self ._size [0 ] + goal_size [0 ] + self ._fb_offset , 0 ,
179363 goal_size [2 ]),
180364 size = goal_size ,
181- rgba = (0 , 0 , 1 , 0.5 ),
365+ rgba = (.2 , .2 , 1 , 0.5 ),
182366 visible = True ,
183367 name = 'home_goal' )
184368 self .attach (self ._home_goal )
185369
186- self ._away_goal = props .PositionDetector (
370+ self ._away_goal = Goal (
371+ direction = - 1 ,
372+ make_net = False ,
187373 pos = (self ._size [0 ] - goal_size [0 ] - self ._fb_offset , 0 , goal_size [2 ]),
188374 size = goal_size ,
189- rgba = (1 , 0 , 0 , 0.5 ),
375+ rgba = (1 , .2 , .2 , 0.5 ),
190376 visible = True ,
191377 name = 'away_goal' )
192378 self .attach (self ._away_goal )
0 commit comments