Skip to content

Commit 785e06d

Browse files
yuvaltassacopybara-github
authored andcommitted
Replace PositionDetector with Goal subclass for soccer pitch.
PiperOrigin-RevId: 307384858 Change-Id: I29d3c7b0e2e018ad5371d7b3ce7f39af45fcce93
1 parent 50634a5 commit 785e06d

3 files changed

Lines changed: 194 additions & 8 deletions

File tree

dm_control/entities/props/position_detector.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ def _is_in_zone(self, xpos):
225225
and np.all(self._upper > xpos[:len(self._upper)]))
226226

227227
def _update_detection(self, physics):
228-
previously_detected = self._detected
228+
self._previously_detected = self._detected
229229
self._detected = False
230230
for detection in self._entities:
231231
detection.detected = False
@@ -235,9 +235,9 @@ def _update_detection(self, physics):
235235
self._detected = True
236236
break
237237

238-
if self._detected and not previously_detected:
238+
if self._detected and not self._previously_detected:
239239
physics.bind(self._site).rgba = self._detected_rgba
240-
elif previously_detected and not self._detected:
240+
elif self._previously_detected and not self._detected:
241241
physics.bind(self._site).rgba = self._rgba
242242

243243
def site_pos(self, physics):

dm_control/locomotion/soccer/pitch.py

Lines changed: 190 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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

5576
def _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+
87268
class 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)

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ def find_data_files(package_dir, patterns):
166166

167167
setup(
168168
name='dm_control',
169-
version='0.0.307033472',
169+
version='0.0.307384858',
170170
description='Continuous control environments and MuJoCo Python bindings.',
171171
author='DeepMind',
172172
license='Apache License, Version 2.0',

0 commit comments

Comments
 (0)