# MIT License # Copyright (c) 2018 OpenAI # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ 2D rendering framework """ from __future__ import division import os import six import sys from gym import error import math import numpy as np import pyglet from pyglet.gl import glEnable, glHint, glLineWidth, glBlendFunc, glClearColor, glPushMatrix, \ glTranslatef, glRotatef, glScalef, glPopMatrix, glColor4f, glBegin, glVertex3f, glEnd, glLineStipple, \ glDisable, glVertex2f, GL_BLEND, GL_LINE_SMOOTH, GL_LINE_SMOOTH_HINT, GL_NICEST, GL_SRC_ALPHA, \ GL_ONE_MINUS_SRC_ALPHA, GL_LINE_STIPPLE, GL_POINTS, GL_QUADS, GL_TRIANGLES, GL_POLYGON, GL_LINE_LOOP, \ GL_LINE_STRIP, GL_LINES if "Apple" in sys.version: if 'DYLD_FALLBACK_LIBRARY_PATH' in os.environ: os.environ['DYLD_FALLBACK_LIBRARY_PATH'] += ':/usr/lib' # (JDS 2016/04/15): avoid bug on Anaconda 2.3.0 / Yosemite RAD2DEG = 57.29577951308232 def get_display(spec): """Convert a display specification (such as :0) into an actual Display object. Pyglet only supports multiple Displays on Linux. """ if spec is None: return None elif isinstance(spec, six.string_types): return pyglet.canvas.Display(spec) else: raise error.Error('Invalid display specification: {}. (Must be a string like :0 or None.)'.format(spec)) class Viewer(object): def __init__(self, width, height, display=None): display = get_display(display) self.width = width self.height = height self.window = pyglet.window.Window(width=width, height=height, display=display) self.window.on_close = self.window_closed_by_user self.geoms = [] self.onetime_geoms = [] self.transform = Transform() glEnable(GL_BLEND) # glEnable(GL_MULTISAMPLE) glEnable(GL_LINE_SMOOTH) # glHint(GL_LINE_SMOOTH_HINT, GL_DONT_CARE) glHint(GL_LINE_SMOOTH_HINT, GL_NICEST) glLineWidth(2.0) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) def close(self): self.window.close() def window_closed_by_user(self): self.close() def set_bounds(self, left, right, bottom, top): assert right > left and top > bottom scalex = self.width / (right - left) scaley = self.height / (top - bottom) self.transform = Transform( translation=(-left * scalex, -bottom * scaley), scale=(scalex, scaley)) def add_geom(self, geom): self.geoms.append(geom) def add_onetime(self, geom): self.onetime_geoms.append(geom) def render(self, return_rgb_array=False): glClearColor(1, 1, 1, 1) self.window.clear() self.window.switch_to() self.window.dispatch_events() self.transform.enable() for geom in self.geoms: geom.render() for geom in self.onetime_geoms: geom.render() self.transform.disable() arr = None if return_rgb_array: buffer = pyglet.image.get_buffer_manager().get_color_buffer() image_data = buffer.get_image_data() arr = np.fromstring(image_data.data, dtype=np.uint8, sep='') # In https://github.com/openai/gym-http-api/issues/2, we # discovered that someone using Xmonad on Arch was having # a window of size 598 x 398, though a 600 x 400 window # was requested. (Guess Xmonad was preserving a pixel for # the boundary.) So we use the buffer height/width rather # than the requested one. arr = arr.reshape(buffer.height, buffer.width, 4) arr = arr[::-1, :, 0:3] self.window.flip() self.onetime_geoms = [] return arr # Convenience def draw_circle(self, radius=10, res=30, filled=True, **attrs): geom = make_circle(radius=radius, res=res, filled=filled) _add_attrs(geom, attrs) self.add_onetime(geom) return geom def draw_polygon(self, v, filled=True, **attrs): geom = make_polygon(v=v, filled=filled) _add_attrs(geom, attrs) self.add_onetime(geom) return geom def draw_polyline(self, v, **attrs): geom = make_polyline(v=v) _add_attrs(geom, attrs) self.add_onetime(geom) return geom def draw_line(self, start, end, **attrs): geom = Line(start, end) _add_attrs(geom, attrs) self.add_onetime(geom) return geom def get_array(self): self.window.flip() image_data = pyglet.image.get_buffer_manager().get_color_buffer().get_image_data() self.window.flip() arr = np.fromstring(image_data.data, dtype=np.uint8, sep='') arr = arr.reshape(self.height, self.width, 4) return arr[::-1, :, 0:3] def _add_attrs(geom, attrs): if "color" in attrs: geom.set_color(*attrs["color"]) if "linewidth" in attrs: geom.set_linewidth(attrs["linewidth"]) class Geom(object): def __init__(self): self._color = Color((0, 0, 0, 1.0)) self.attrs = [self._color] def render(self): for attr in reversed(self.attrs): attr.enable() self.render1() for attr in self.attrs: attr.disable() def render1(self): raise NotImplementedError def add_attr(self, attr): self.attrs.append(attr) def set_color(self, r, g, b, alpha=1): self._color.vec4 = (r, g, b, alpha) class Attr(object): def enable(self): raise NotImplementedError def disable(self): pass class Transform(Attr): def __init__(self, translation=(0.0, 0.0), rotation=0.0, scale=(1, 1)): self.set_translation(*translation) self.set_rotation(rotation) self.set_scale(*scale) def enable(self): glPushMatrix() glTranslatef(self.translation[0], self.translation[1], 0) # translate to GL loc ppint glRotatef(RAD2DEG * self.rotation, 0, 0, 1.0) glScalef(self.scale[0], self.scale[1], 1) def disable(self): glPopMatrix() def set_translation(self, newx, newy): self.translation = (float(newx), float(newy)) def set_rotation(self, new): self.rotation = float(new) def set_scale(self, newx, newy): self.scale = (float(newx), float(newy)) class Color(Attr): def __init__(self, vec4): self.vec4 = vec4 def enable(self): glColor4f(*self.vec4) class LineStyle(Attr): def __init__(self, style): self.style = style def enable(self): glEnable(GL_LINE_STIPPLE) glLineStipple(1, self.style) def disable(self): glDisable(GL_LINE_STIPPLE) class LineWidth(Attr): def __init__(self, stroke): self.stroke = stroke def enable(self): glLineWidth(self.stroke) class Point(Geom): def __init__(self): Geom.__init__(self) def render1(self): glBegin(GL_POINTS) # draw point glVertex3f(0.0, 0.0, 0.0) glEnd() class FilledPolygon(Geom): def __init__(self, v): Geom.__init__(self) self.v = v def render1(self): if len(self.v) == 4: glBegin(GL_QUADS) elif len(self.v) > 4: glBegin(GL_POLYGON) else: glBegin(GL_TRIANGLES) for p in self.v: glVertex3f(p[0], p[1], 0) # draw each vertex glEnd() color = ( self._color.vec4[0] * 0.5, self._color.vec4[1] * 0.5, self._color.vec4[2] * 0.5, self._color.vec4[3] * 0.5) glColor4f(*color) glBegin(GL_LINE_LOOP) for p in self.v: glVertex3f(p[0], p[1], 0) # draw each vertex glEnd() def make_circle(radius=10, res=30, filled=True): points = [] for i in range(res): ang = 2 * math.pi * i / res points.append((math.cos(ang) * radius, math.sin(ang) * radius)) if filled: return FilledPolygon(points) else: return PolyLine(points, True) def make_polygon(v, filled=True): if filled: return FilledPolygon(v) else: return PolyLine(v, True) def make_polyline(v): return PolyLine(v, False) def make_capsule(length, width): l, r, t, b = 0, length, width / 2, -width / 2 box = make_polygon([(l, b), (l, t), (r, t), (r, b)]) circ0 = make_circle(width / 2) circ1 = make_circle(width / 2) circ1.add_attr(Transform(translation=(length, 0))) geom = Compound([box, circ0, circ1]) return geom class Compound(Geom): def __init__(self, gs): Geom.__init__(self) self.gs = gs for g in self.gs: g.attrs = [a for a in g.attrs if not isinstance(a, Color)] def render1(self): for g in self.gs: g.render() class PolyLine(Geom): def __init__(self, v, close): Geom.__init__(self) self.v = v self.close = close self.linewidth = LineWidth(1) self.add_attr(self.linewidth) def render1(self): glBegin(GL_LINE_LOOP if self.close else GL_LINE_STRIP) for p in self.v: glVertex3f(p[0], p[1], 0) # draw each vertex glEnd() def set_linewidth(self, x): self.linewidth.stroke = x class Line(Geom): def __init__(self, start=(0.0, 0.0), end=(0.0, 0.0)): Geom.__init__(self) self.start = start self.end = end self.linewidth = LineWidth(1) self.add_attr(self.linewidth) def render1(self): glBegin(GL_LINES) glVertex2f(*self.start) glVertex2f(*self.end) glEnd() class Image(Geom): def __init__(self, fname, width, height): Geom.__init__(self) self.width = width self.height = height img = pyglet.image.load(fname) self.img = img self.flip = False def render1(self): self.img.blit(-self.width / 2, -self.height / 2, width=self.width, height=self.height) class SimpleImageViewer(object): def __init__(self, display=None): self.window = None self.isopen = False self.display = display def imshow(self, arr): if self.window is None: height, width, channels = arr.shape self.window = pyglet.window.Window(width=width, height=height, display=self.display) self.width = width self.height = height self.isopen = True assert arr.shape == (self.height, self.width, 3), "You passed in an image with the wrong number shape" image = pyglet.image.ImageData(self.width, self.height, 'RGB', arr.tobytes(), pitch=self.width * -3) self.window.clear() self.window.switch_to() self.window.dispatch_events() image.blit(0, 0) self.window.flip() def close(self): if self.isopen: self.window.close() self.isopen = False def __del__(self): self.close()