mirror of
https://github.com/Farama-Foundation/Gymnasium.git
synced 2025-08-19 13:32:03 +00:00
In gym.envs.classic_control.rendering, Geom has a color attribute, which is initialized as black. This is a good default for most Geom subclasses, such as lines and circles. However, in the case of Image instances, the result is a completely black image - in the case of an image without transparency, the result is a black square. This can be easily fixed by setting the default color for Image instances as white, which causes images to be displayed normally.
383 lines
12 KiB
Python
383 lines
12 KiB
Python
"""
|
|
2D rendering framework
|
|
"""
|
|
import os
|
|
import sys
|
|
|
|
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
|
|
|
|
from gym import error
|
|
|
|
try:
|
|
import pyglet
|
|
except ImportError as e:
|
|
raise ImportError('''
|
|
Cannot import pyglet.
|
|
HINT: you can install pyglet directly via 'pip install pyglet'.
|
|
But if you really just want to install all Gym dependencies and not have to think about it,
|
|
'pip install -e .[all]' or 'pip install gym[all]' will do it.
|
|
''')
|
|
|
|
try:
|
|
from pyglet.gl import *
|
|
except ImportError as e:
|
|
raise ImportError('''
|
|
Error occurred while running `from pyglet.gl import *`
|
|
HINT: make sure you have OpenGL install. On Ubuntu, you can run 'apt-get install python-opengl'.
|
|
If you're running on a server, you may need a virtual frame buffer; something like this should work:
|
|
'xvfb-run -s \"-screen 0 1400x900x24\" python <your_script.py>'
|
|
''')
|
|
|
|
import math
|
|
import numpy as np
|
|
|
|
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 pyglet.canvas.get_display()
|
|
# returns already available pyglet_display,
|
|
# if there is no pyglet display available then it creates one
|
|
elif isinstance(spec, str):
|
|
return pyglet.canvas.Display(spec)
|
|
else:
|
|
raise error.Error('Invalid display specification: {}. (Must be a string like :0 or None.)'.format(spec))
|
|
|
|
def get_window(width, height, display, **kwargs):
|
|
"""
|
|
Will create a pyglet window from the display specification provided.
|
|
"""
|
|
screen = display.get_screens() #available screens
|
|
config = screen[0].get_best_config() #selecting the first screen
|
|
context = config.create_context(None) #create GL context
|
|
|
|
return pyglet.window.Window(width=width, height=height, display=display, config=config, context=context, **kwargs)
|
|
|
|
class Viewer(object):
|
|
def __init__(self, width, height, display=None):
|
|
display = get_display(display)
|
|
|
|
self.width = width
|
|
self.height = height
|
|
self.window = get_window(width=width, height=height, display=display)
|
|
self.window.on_close = self.window_closed_by_user
|
|
self.isopen = True
|
|
self.geoms = []
|
|
self.onetime_geoms = []
|
|
self.transform = Transform()
|
|
|
|
glEnable(GL_BLEND)
|
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
|
|
|
def close(self):
|
|
if self.isopen and sys.meta_path:
|
|
# ^^^ check sys.meta_path to avoid 'ImportError: sys.meta_path is None, Python is likely shutting down'
|
|
self.window.close()
|
|
self.isopen = False
|
|
|
|
def window_closed_by_user(self):
|
|
self.isopen = False
|
|
|
|
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.frombuffer(image_data.get_data(), dtype=np.uint8)
|
|
# 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 if return_rgb_array else self.isopen
|
|
|
|
# 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.get_data(), dtype=np.uint8, sep='')
|
|
arr = arr.reshape(self.height, self.width, 4)
|
|
return arr[::-1,:,0:3]
|
|
|
|
def __del__(self):
|
|
self.close()
|
|
|
|
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):
|
|
self._color.vec4 = (r, g, b, 1)
|
|
|
|
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()
|
|
|
|
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.set_color(1.0, 1.0, 1.0)
|
|
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, maxwidth=500):
|
|
self.window = None
|
|
self.isopen = False
|
|
self.display = get_display(display)
|
|
self.maxwidth = maxwidth
|
|
def imshow(self, arr):
|
|
if self.window is None:
|
|
height, width, _channels = arr.shape
|
|
if width > self.maxwidth:
|
|
scale = self.maxwidth / width
|
|
width = int(scale * width)
|
|
height = int(scale * height)
|
|
self.window = get_window(width=width, height=height, display=self.display, vsync=False, resizable=True)
|
|
self.width = width
|
|
self.height = height
|
|
self.isopen = True
|
|
|
|
@self.window.event
|
|
def on_resize(width, height):
|
|
self.width = width
|
|
self.height = height
|
|
|
|
@self.window.event
|
|
def on_close():
|
|
self.isopen = False
|
|
|
|
assert len(arr.shape) == 3, "You passed in an image with the wrong number shape"
|
|
image = pyglet.image.ImageData(arr.shape[1], arr.shape[0],
|
|
'RGB', arr.tobytes(), pitch=arr.shape[1]*-3)
|
|
gl.glTexParameteri(gl.GL_TEXTURE_2D,
|
|
gl.GL_TEXTURE_MAG_FILTER, gl.GL_NEAREST)
|
|
texture = image.get_texture()
|
|
texture.width = self.width
|
|
texture.height = self.height
|
|
self.window.clear()
|
|
self.window.switch_to()
|
|
self.window.dispatch_events()
|
|
texture.blit(0, 0) # draw
|
|
self.window.flip()
|
|
def close(self):
|
|
if self.isopen and sys.meta_path:
|
|
# ^^^ check sys.meta_path to avoid 'ImportError: sys.meta_path is None, Python is likely shutting down'
|
|
self.window.close()
|
|
self.isopen = False
|
|
|
|
def __del__(self):
|
|
self.close()
|