You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

opengl.py 58KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489
  1. # Copyright (c) 2017 Anki, Inc.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License in the file LICENSE.txt or at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. '''This module provides a 3D visualizer for Cozmo's world state.
  15. It uses PyOpenGL, a Python OpenGL 3D graphics library which is available on most
  16. platforms. It also depends on the Pillow library for image processing.
  17. The easiest way to make use of this viewer is to call :func:`cozmo.run_program`
  18. with `use_3d_viewer=True` or :func:`cozmo.run.connect_with_3dviewer`.
  19. Warning:
  20. This package requires Python to have the PyOpenGL package installed, along
  21. with an implementation of GLUT (OpenGL Utility Toolkit).
  22. To install the Python packages do ``pip3 install --user "cozmo[3dviewer]"``
  23. On Windows and Linux you must also install freeglut (macOS / OSX has one
  24. preinstalled).
  25. On Linux: ``sudo apt-get install freeglut3``
  26. On Windows: Go to http://freeglut.sourceforge.net/ to get a ``freeglut.dll``
  27. file. It's included in any of the `Windows binaries` downloads. Place the DLL
  28. next to your Python script, or install it somewhere in your PATH to allow any
  29. script to use it."
  30. '''
  31. # __all__ should order by constants, event classes, other classes, functions.
  32. __all__ = ['DynamicTexture', 'LoadedObjFile', 'OpenGLViewer', 'OpenGLWindow',
  33. 'RenderableObject',
  34. 'LoadMtlFile']
  35. import collections
  36. import math
  37. import time
  38. from pkg_resources import resource_stream
  39. from OpenGL.GL import *
  40. from OpenGL.GLU import *
  41. from OpenGL.GLUT import *
  42. from PIL import Image
  43. from .exceptions import InvalidOpenGLGlutImplementation, RobotBusy
  44. from . import logger
  45. from . import nav_memory_map
  46. from . import objects
  47. from . import robot
  48. from . import util
  49. from . import world
  50. # Check if OpenGL imported correctly and bound to a valid GLUT implementation
  51. def _glut_install_instructions():
  52. if sys.platform.startswith('linux'):
  53. return "Install freeglut: `sudo apt-get install freeglut3`"
  54. elif sys.platform.startswith('darwin'):
  55. return "GLUT should already be installed by default on macOS!"
  56. elif sys.platform in ('win32', 'cygwin'):
  57. return "Install freeglut: You can download it from http://freeglut.sourceforge.net/ \n"\
  58. "You just need the `freeglut.dll` file, from any of the 'Windows binaries' downloads. "\
  59. "Place the DLL next to your Python script, or install it somewhere in your PATH "\
  60. "to allow any script to use it."
  61. else:
  62. return "(Instructions unknown for platform %s)" % sys.platform
  63. def _verify_glut_init():
  64. # According to the documentation, just checking bool(glutInit) is supposed to be enough
  65. # However on Windows with no GLUT DLL that can still pass, even if calling the method throws a null function error.
  66. if bool(glutInit):
  67. try:
  68. glutInit()
  69. return True
  70. except OpenGL.error.NullFunctionError as e:
  71. pass
  72. return False
  73. if not _verify_glut_init():
  74. raise InvalidOpenGLGlutImplementation(_glut_install_instructions())
  75. _resource_package = __name__ # All resources are in subdirectories from this file's location
  76. # Global viewer instance
  77. opengl_viewer = None # type: OpenGLViewer
  78. class DynamicTexture:
  79. """Wrapper around An OpenGL Texture that can be dynamically updated."""
  80. def __init__(self):
  81. self._texId = glGenTextures(1)
  82. self._width = None
  83. self._height = None
  84. # Bind an ID for this texture
  85. glBindTexture(GL_TEXTURE_2D, self._texId)
  86. # Use bilinear filtering if the texture has to be scaled
  87. glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
  88. glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
  89. def bind(self):
  90. """Bind the texture for rendering."""
  91. glBindTexture(GL_TEXTURE_2D, self._texId)
  92. def update(self, pil_image: Image.Image):
  93. """Update the texture to contain the provided image.
  94. Args:
  95. pil_image (PIL.Image.Image): The image to write into the texture.
  96. """
  97. # Ensure the image is in RGBA format and convert to the raw RGBA bytes.
  98. image_width, image_height = pil_image.size
  99. image = pil_image.convert("RGBA").tobytes("raw", "RGBA")
  100. # Bind the texture so that it can be modified.
  101. self.bind()
  102. if (self._width==image_width) and (self._height==image_height):
  103. # Same size - just need to update the texels.
  104. glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, image_width, image_height,
  105. GL_RGBA, GL_UNSIGNED_BYTE, image)
  106. else:
  107. # Different size than the last frame (e.g. the Window is resizing)
  108. # Create a new texture of the correct size.
  109. glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image_width, image_height,
  110. 0, GL_RGBA, GL_UNSIGNED_BYTE, image)
  111. self._width = image_width
  112. self._height = image_height
  113. def LoadMtlFile(filename):
  114. """Load a .mtl material file, and return the contents as a dictionary.
  115. Supports the subset of MTL required for the Cozmo 3D viewer assets.
  116. Args:
  117. filename (str): The filename of the file to load.
  118. Returns:
  119. dict: A dictionary mapping named MTL attributes to values.
  120. """
  121. contents = {}
  122. current_mtl = None
  123. resource_path = '/'.join(('assets', filename)) # Note: Deliberately not os.path.join, for use with pkg_resources
  124. file_data = resource_stream(_resource_package, resource_path)
  125. for line in file_data:
  126. line = line.decode("utf-8") # Convert bytes line to a string
  127. if line.startswith('#'):
  128. # ignore comments in the file
  129. continue
  130. values = line.split()
  131. if not values:
  132. # ignore empty lines
  133. continue
  134. attribute_name = values[0]
  135. if attribute_name == 'newmtl':
  136. # Create a new empty material
  137. current_mtl = contents[values[1]] = {}
  138. elif current_mtl is None:
  139. raise ValueError("mtl file must start with newmtl statement")
  140. elif attribute_name == 'map_Kd':
  141. # Diffuse texture map - load the image into memory
  142. image_name = values[1]
  143. image_resource_path = '/'.join(('assets', image_name)) # Note: Deliberately not os.path.join, for use with pkg_resources
  144. image_file_data = resource_stream(_resource_package, image_resource_path)
  145. with Image.open(image_file_data) as image:
  146. image_width, image_height = image.size
  147. image = image.convert("RGBA").tobytes("raw", "RGBA")
  148. # Bind the image as a texture that can be used for rendering
  149. texture_id = glGenTextures(1)
  150. current_mtl['texture_Kd'] = texture_id
  151. glBindTexture(GL_TEXTURE_2D, texture_id)
  152. glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
  153. glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
  154. glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image_width, image_height,
  155. 0, GL_RGBA, GL_UNSIGNED_BYTE, image)
  156. else:
  157. # Store the values for this attribute as a list of float values
  158. current_mtl[attribute_name] = list(map(float, values[1:]))
  159. # File loaded successfully - return the contents
  160. return contents
  161. class LoadedObjFile:
  162. """The loaded / parsed contents of a 3D Wavefront OBJ file.
  163. This is the intermediary step between the file on the disk, and a renderable
  164. 3D object. It supports the subset of the OBJ file that was used in the
  165. Cozmo and Cube assets, and does not attempt to exhaustively support every
  166. possible setting.
  167. Args:
  168. filename (str): The filename of the OBJ file to load.
  169. """
  170. def __init__(self, filename):
  171. # list: The vertices (each vertex stored as list of 3 floats).
  172. self.vertices = []
  173. # list: The vertex normals (each normal stored as list of 3 floats).
  174. self.normals = []
  175. # list: The texture coordinates (each coordinate stored as list of 2 floats).
  176. self.tex_coords = []
  177. # dict: The faces for each mesh, indexed by mesh name.
  178. self.mesh_faces = {}
  179. # dict: A dictionary mapping named MTL attributes to values.
  180. self.mtl = None
  181. group_name = None
  182. material = None
  183. resource_path = '/'.join(('assets', filename)) # Note: Deliberately not os.path.join, for use with pkg_resources
  184. file_data = resource_stream(_resource_package, resource_path)
  185. for line in file_data:
  186. line = line.decode("utf-8") # Convert bytes to string
  187. if line.startswith('#'):
  188. # ignore comments in the file
  189. continue
  190. values = line.split()
  191. if not values:
  192. # ignore empty lines
  193. continue
  194. if values[0] == 'v':
  195. # vertex position
  196. v = list(map(float, values[1:4]))
  197. self.vertices.append(v)
  198. elif values[0] == 'vn':
  199. # vertex normal
  200. v = list(map(float, values[1:4]))
  201. self.normals.append(v)
  202. elif values[0] == 'vt':
  203. # texture coordinate
  204. self.tex_coords.append(list(map(float, values[1:3])))
  205. elif values[0] in ('usemtl', 'usemat'):
  206. # material
  207. material = values[1]
  208. elif values[0] == 'mtllib':
  209. # material library (a filename)
  210. self.mtl = LoadMtlFile(values[1])
  211. elif values[0] == 'f':
  212. # A face made up of 3 or 4 vertices - e.g. `f v1 v2 v3` or `f v1 v2 v3 v4`
  213. # where each vertex definition is multiple indexes seperated by
  214. # slashes and can follow the following formats:
  215. # position_index
  216. # position_index/tex_coord_index
  217. # position_index/tex_coord_index/normal_index
  218. # position_index//normal_index
  219. positions = []
  220. tex_coords = []
  221. normals = []
  222. for vertex in values[1:]:
  223. vertex_components = vertex.split('/')
  224. positions.append(int(vertex_components[0]))
  225. # There's only a texture coordinate if there's at least 2 entries and the 2nd entry is non-zero length
  226. if len(vertex_components) >= 2 and len(vertex_components[1]) > 0:
  227. tex_coords.append(int(vertex_components[1]))
  228. else:
  229. # OBJ file indexing starts at 1, so use 0 to indicate no entry
  230. tex_coords.append(0)
  231. # There's only a normal if there's at least 2 entries and the 2nd entry is non-zero length
  232. if len(vertex_components) >= 3 and len(vertex_components[2]) > 0:
  233. normals.append(int(vertex_components[2]))
  234. else:
  235. # OBJ file indexing starts at 1, so use 0 to indicate no entry
  236. normals.append(0)
  237. try:
  238. mesh_face = self.mesh_faces[group_name]
  239. except KeyError:
  240. # Create a new mesh group
  241. self.mesh_faces[group_name] = []
  242. mesh_face = self.mesh_faces[group_name]
  243. mesh_face.append((positions, normals, tex_coords, material))
  244. elif values[0] == 'o':
  245. # object name - ignore
  246. pass
  247. elif values[0] == 'g':
  248. # group name (for a sub-mesh)
  249. group_name = values[1]
  250. elif values[0] == 's':
  251. # smooth shading (1..20, and 'off') - ignore
  252. pass
  253. else:
  254. logger.warning("LoadedObjFile Ignoring unhandled type '%s' in line %s",
  255. values[0], values)
  256. class RenderableObject:
  257. """Container for an object that can be rendered via OpenGL.
  258. Can contain multiple meshes, for e.g. articulated objects.
  259. Args:
  260. object_data (LoadedObjFile): The object data (vertices, faces, etc.)
  261. to generate the renderable object from.
  262. override_mtl (dict): An optional material to use as an override instead
  263. of the material specified in the data. This allows one OBJ file
  264. to be used to create multiple objects with different materials
  265. and textures. Use :meth:`LoadMtlFile` to generate a dict from a
  266. MTL file.
  267. """
  268. def __init__(self, object_data: LoadedObjFile, override_mtl=None):
  269. #: dict: The individual meshes, indexed by name, for this object.
  270. self.meshes = {}
  271. mtl_dict = override_mtl if (override_mtl is not None) else object_data.mtl
  272. def _as_rgba(color):
  273. if len(color) >= 4:
  274. return color
  275. else:
  276. # RGB - add alpha defaulted to 1
  277. return color + [1.0]
  278. for key in object_data.mesh_faces:
  279. new_gl_list = glGenLists(1)
  280. glNewList(new_gl_list, GL_COMPILE)
  281. self.meshes[key] = new_gl_list
  282. part_faces = object_data.mesh_faces[key]
  283. glEnable(GL_TEXTURE_2D)
  284. glFrontFace(GL_CCW)
  285. for face in part_faces:
  286. vertices, normals, texture_coords, material = face
  287. mtl = mtl_dict[material]
  288. if 'texture_Kd' in mtl:
  289. # use diffuse texture map
  290. glBindTexture(GL_TEXTURE_2D, mtl['texture_Kd'])
  291. else:
  292. # No texture map
  293. glBindTexture(GL_TEXTURE_2D, 0)
  294. # Diffuse light
  295. mtl_kd_rgba = _as_rgba(mtl['Kd'])
  296. glColor(mtl_kd_rgba)
  297. # Ambient light
  298. if 'Ka' in mtl:
  299. mtl_ka_rgba = _as_rgba(mtl['Ka'])
  300. glMaterialfv(GL_FRONT, GL_AMBIENT, mtl_ka_rgba)
  301. glMaterialfv(GL_FRONT, GL_DIFFUSE, mtl_kd_rgba)
  302. else:
  303. glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, mtl_kd_rgba);
  304. # Specular light
  305. if 'Ks' in mtl:
  306. mtl_ks_rgba = _as_rgba(mtl['Ks'])
  307. glMaterialfv(GL_FRONT, GL_SPECULAR, mtl_ks_rgba);
  308. if 'Ns' in mtl:
  309. specular_exponent = mtl['Ns']
  310. glMaterialfv(GL_FRONT, GL_SHININESS, specular_exponent);
  311. # Polygon (N verts) with optional normals and tex coords
  312. glBegin(GL_POLYGON)
  313. for i in range(len(vertices)):
  314. normal_index = normals[i]
  315. if normal_index > 0:
  316. glNormal3fv(object_data.normals[normal_index - 1])
  317. tex_coord_index = texture_coords[i]
  318. if tex_coord_index > 0:
  319. glTexCoord2fv( object_data.tex_coords[tex_coord_index - 1])
  320. glVertex3fv(object_data.vertices[vertices[i] - 1])
  321. glEnd()
  322. glDisable(GL_TEXTURE_2D)
  323. glEndList()
  324. def draw_all(self):
  325. """Draw all of the meshes."""
  326. for mesh in self.meshes.values():
  327. glCallList(mesh)
  328. def _make_unit_cube():
  329. """Make a unit-size cube, with normals, centered at the origin"""
  330. new_gl_list = glGenLists(1)
  331. glNewList(new_gl_list, GL_COMPILE)
  332. # build each of the 6 faces
  333. for face_index in range(6):
  334. # calculate normal and vertices for this face
  335. vertex_normal = [0.0, 0.0, 0.0]
  336. vertex_pos_options1 = [-1.0, 1.0, 1.0, -1.0]
  337. vertex_pos_options2 = [ 1.0, 1.0, -1.0, -1.0]
  338. face_index_even = ((face_index % 2) == 0)
  339. # odd and even faces point in opposite directions
  340. normal_dir = 1.0 if face_index_even else -1.0
  341. if face_index < 2:
  342. # -X and +X faces (vert positions differ in Y,Z)
  343. vertex_normal[0] = normal_dir
  344. v1i = 1
  345. v2i = 2
  346. elif face_index < 4:
  347. # -Y and +Y faces (vert positions differ in X,Z)
  348. vertex_normal[1] = normal_dir
  349. v1i = 0
  350. v2i = 2
  351. else:
  352. # -Z and +Z faces (vert positions differ in X,Y)
  353. vertex_normal[2] = normal_dir
  354. v1i = 0
  355. v2i = 1
  356. vertex_pos = list(vertex_normal)
  357. # Polygon (N verts) with optional normals and tex coords
  358. glBegin(GL_POLYGON)
  359. for vert_index in range(4):
  360. vertex_pos[v1i] = vertex_pos_options1[vert_index]
  361. vertex_pos[v2i] = vertex_pos_options2[vert_index]
  362. glNormal3fv(vertex_normal)
  363. glVertex3fv(vertex_pos)
  364. glEnd()
  365. glEndList()
  366. return new_gl_list
  367. class OpenGLWindow():
  368. """A Window displaying an OpenGL viewport.
  369. Args:
  370. x (int): The initial x coordinate of the window in pixels.
  371. y (int): The initial y coordinate of the window in pixels.
  372. width (int): The initial height of the window in pixels.
  373. height (int): The initial height of the window in pixels.
  374. window_name (str): The name / title for the window.
  375. is_3d (bool): True to create a Window for 3D rendering.
  376. """
  377. def __init__(self, x, y, width, height, window_name, is_3d):
  378. self._pos = (x, y)
  379. #: int: The width of the window
  380. self.width = width
  381. #: int: The height of the window
  382. self.height = height
  383. self._gl_window = None
  384. self._window_name = window_name
  385. self._is_3d = is_3d
  386. def init_display(self):
  387. """Initialze the OpenGL display parts of the Window.
  388. Warning:
  389. Must be called on the same thread as OpenGL (usually the main thread),
  390. and after glutInit().
  391. """
  392. glutInitWindowSize(self.width, self.height)
  393. glutInitWindowPosition(*self._pos)
  394. self.gl_window = glutCreateWindow(self._window_name)
  395. if self._is_3d:
  396. glClearColor(0, 0, 0, 0)
  397. glEnable(GL_DEPTH_TEST)
  398. glShadeModel(GL_SMOOTH)
  399. glutReshapeFunc(self._reshape)
  400. def _reshape(self, width, height):
  401. # Called from OpenGL whenever this window is resized.
  402. self.width = width
  403. self.height = height
  404. glViewport(0, 0, width, height)
  405. class RobotRenderFrame():
  406. """Minimal copy of a Robot's state for 1 frame of rendering."""
  407. def __init__(self, robot):
  408. self.pose = robot.pose
  409. self.head_angle = robot.head_angle
  410. self.lift_position = robot.lift_position
  411. class ObservableElementRenderFrame():
  412. """Minimal copy of a Cube's state for 1 frame of rendering."""
  413. def __init__(self, element):
  414. self.pose = element.pose
  415. self.is_visible = element.is_visible
  416. self.last_observed_time = element.last_observed_time
  417. @property
  418. def time_since_last_seen(self):
  419. # Equivalent of ObservableElement's method
  420. '''float: time since this element was last seen (math.inf if never)'''
  421. if self.last_observed_time is None:
  422. return math.inf
  423. return time.time() - self.last_observed_time
  424. class CubeRenderFrame(ObservableElementRenderFrame):
  425. """Minimal copy of a Cube's state for 1 frame of rendering."""
  426. def __init__(self, cube):
  427. super().__init__(cube)
  428. class FaceRenderFrame(ObservableElementRenderFrame):
  429. """Minimal copy of a Face's state for 1 frame of rendering."""
  430. def __init__(self, face):
  431. super().__init__(face)
  432. class CustomObjectRenderFrame(ObservableElementRenderFrame):
  433. """Minimal copy of a CustomObject's state for 1 frame of rendering."""
  434. def __init__(self, obj, is_fixed):
  435. if is_fixed:
  436. # Not an observable, so init directly
  437. self.pose = obj.pose
  438. self.is_visible = None
  439. self.last_observed_time = None
  440. else:
  441. super().__init__(obj)
  442. self.is_fixed = is_fixed
  443. self.x_size_mm = obj.x_size_mm
  444. self.y_size_mm = obj.y_size_mm
  445. self.z_size_mm = obj.z_size_mm
  446. class WorldRenderFrame():
  447. """Minimal copy of the World's state for 1 frame of rendering."""
  448. def __init__(self, robot):
  449. world = robot.world
  450. self.robot_frame = RobotRenderFrame(robot)
  451. self.cube_frames = []
  452. for i in range(3):
  453. cube_id = objects.LightCubeIDs[i]
  454. cube = world.get_light_cube(cube_id)
  455. if cube is None:
  456. self.cube_frames.append(None)
  457. else:
  458. self.cube_frames.append(CubeRenderFrame(cube))
  459. self.face_frames = []
  460. for face in world._faces.values():
  461. # Ignore faces that have a newer version (with updated id)
  462. # or if they haven't been seen in a while).
  463. if not face.has_updated_face_id and (face.time_since_last_seen < 60):
  464. self.face_frames.append(FaceRenderFrame(face))
  465. self.custom_object_frames = []
  466. for obj in world._objects.values():
  467. is_custom = isinstance(obj, objects.CustomObject)
  468. is_fixed = isinstance(obj, objects.FixedCustomObject)
  469. if is_custom or is_fixed:
  470. self.custom_object_frames.append(CustomObjectRenderFrame(obj, is_fixed))
  471. class RobotControlIntents():
  472. """Input intents for controlling the robot.
  473. These are sent from the OpenGL thread, and consumed by the SDK thread for
  474. issuing movement commands on Cozmo (to provide a remote-control interface).
  475. """
  476. def __init__(self, left_wheel_speed=0.0, right_wheel_speed=0.0,
  477. lift_speed=0.0, head_speed=0.0):
  478. self.left_wheel_speed = left_wheel_speed
  479. self.right_wheel_speed = right_wheel_speed
  480. self.lift_speed = lift_speed
  481. self.head_speed = head_speed
  482. class OpenGLViewer():
  483. """OpenGL based 3D Viewer.
  484. Handles rendering of both a 3D world view and a 2D camera window.
  485. Args:
  486. enable_camera_view (bool): True to also open a 2nd window to display
  487. the live camera view.
  488. show_viewer_controls (bool): Specifies whether to draw controls on the view.
  489. """
  490. def __init__(self, enable_camera_view, show_viewer_controls=True):
  491. # Queues from SDK thread to OpenGL thread
  492. self._img_queue = collections.deque(maxlen=1)
  493. self._nav_memory_map_queue = collections.deque(maxlen=1)
  494. self._world_frame_queue = collections.deque(maxlen=1)
  495. # Queue from OpenGL thread to SDK thread
  496. self._input_intent_queue = collections.deque(maxlen=1)
  497. self._last_robot_control_intents = RobotControlIntents()
  498. self._is_keyboard_control_enabled = False
  499. self._image_handler = None
  500. self._nav_map_handler = None
  501. self._robot_state_handler = None
  502. self._exit_requested = False
  503. global opengl_viewer
  504. if opengl_viewer is not None:
  505. logger.error("Multiple OpenGLViewer instances not expected: "
  506. "OpenGL / GLUT only supports running 1 blocking instance on the main thread.")
  507. opengl_viewer = self
  508. self.main_window = OpenGLWindow(0, 0, 800, 600,
  509. b"Cozmo 3D Visualizer", is_3d=True)
  510. self._camera_view_texture = None # type: DynamicTexture
  511. self.viewer_window = None # type: OpenGLWindow
  512. if enable_camera_view:
  513. self.viewer_window = OpenGLWindow(self.main_window.width, 0, 640, 480,
  514. b"Cozmo CameraFeed", is_3d=False)
  515. self.cozmo_object = None # type: RenderableObject
  516. self.cube_objects = []
  517. self._latest_world_frame = None # type: WorldRenderFrame
  518. self._nav_memory_map_display_list = None
  519. # Keyboard
  520. self._is_key_pressed = {}
  521. self._is_alt_down = False
  522. self._is_ctrl_down = False
  523. self._is_shift_down = False
  524. # Mouse
  525. self._is_mouse_down = {}
  526. self._mouse_pos = None # type: util.Vector2
  527. # Controls
  528. self._show_controls = show_viewer_controls
  529. self._instructions = '\n'.join(['W, S: Move forward, backward',
  530. 'A, D: Turn left, right',
  531. 'R, F: Lift up, down',
  532. 'T, G: Head up, down',
  533. '',
  534. 'LMB: Rotate camera',
  535. 'RMB: Move camera',
  536. 'LMB + RMB: Move camera up/down',
  537. 'LMB + Z: Zoom camera',
  538. 'X: same as RMB',
  539. 'TAB: center view on robot',
  540. '',
  541. 'H: Toggle help'])
  542. # Camera position and orientation defined by a look-at positions
  543. # and a pitch/and yaw to rotate around that along with a distance
  544. self._camera_look_at = util.Vector3(100.0, -25.0, 0.0)
  545. self._camera_pitch = math.radians(40)
  546. self._camera_yaw = math.radians(270)
  547. self._camera_distance = 500.0
  548. self._camera_pos = util.Vector3(0, 0, 0)
  549. self._camera_up = util.Vector3(0.0, 0.0, 1.0)
  550. self._calculate_camera_pos()
  551. def _request_exit(self):
  552. self._exit_requested = True
  553. if bool(glutLeaveMainLoop):
  554. glutLeaveMainLoop()
  555. def _calculate_camera_pos(self):
  556. # Calculate camera position based on look-at, distance and angles
  557. cos_pitch = math.cos(self._camera_pitch)
  558. sin_pitch = math.sin(self._camera_pitch)
  559. cos_yaw = math.cos(self._camera_yaw)
  560. sin_yaw = math.sin(self._camera_yaw)
  561. cam_distance = self._camera_distance
  562. cam_look_at = self._camera_look_at
  563. self._camera_pos._x = cam_look_at.x + (cam_distance * cos_pitch * cos_yaw)
  564. self._camera_pos._y = cam_look_at.y + (cam_distance * cos_pitch * sin_yaw)
  565. self._camera_pos._z = cam_look_at.z + (cam_distance * sin_pitch)
  566. def _update_modifier_keys(self):
  567. modifiers = glutGetModifiers()
  568. self._is_alt_down = (modifiers & GLUT_ACTIVE_ALT != 0)
  569. self._is_ctrl_down = (modifiers & GLUT_ACTIVE_CTRL != 0)
  570. self._is_shift_down = (modifiers & GLUT_ACTIVE_SHIFT != 0)
  571. def _update_intents_for_robot(self):
  572. # Update driving intents based on current input, and pass to SDK thread
  573. # so that it can pass the input on to the robot.
  574. def get_intent_direction(key1, key2):
  575. # Helper for keyboard inputs that have 1 positive and 1 negative input
  576. pos_key = self._is_key_pressed.get(key1, False)
  577. neg_key = self._is_key_pressed.get(key2, False)
  578. return pos_key - neg_key
  579. drive_dir = get_intent_direction(b'w', b's')
  580. turn_dir = get_intent_direction(b'd', b'a')
  581. lift_dir = get_intent_direction(b'r', b'f')
  582. head_dir = get_intent_direction(b't', b'g')
  583. if drive_dir < 0:
  584. # It feels more natural to turn the opposite way when reversing
  585. turn_dir = -turn_dir
  586. # Scale drive speeds with SHIFT (faster) and ALT (slower)
  587. if self._is_shift_down:
  588. speed_scalar = 2.0
  589. elif self._is_alt_down:
  590. speed_scalar = 0.5
  591. else:
  592. speed_scalar = 1.0
  593. drive_speed = 75.0 * speed_scalar
  594. turn_speed = 100.0 * speed_scalar
  595. left_wheel_speed = (drive_dir * drive_speed) + (turn_speed * turn_dir)
  596. right_wheel_speed = (drive_dir * drive_speed) - (turn_speed * turn_dir)
  597. lift_speed = 4.0 * lift_dir * speed_scalar
  598. head_speed = head_dir * speed_scalar
  599. control_intents = RobotControlIntents(left_wheel_speed, right_wheel_speed,
  600. lift_speed, head_speed)
  601. self._input_intent_queue.append(control_intents)
  602. def _idle(self):
  603. if self._is_keyboard_control_enabled:
  604. self._update_intents_for_robot()
  605. glutPostRedisplay()
  606. def _visible(self, vis):
  607. # Called from OpenGL when visibility changes (windows are either visible
  608. # or completely invisible/hidden)
  609. if vis == GLUT_VISIBLE:
  610. glutIdleFunc(self._idle)
  611. else:
  612. glutIdleFunc(None)
  613. def _draw_memory_map(self):
  614. # Update the renderable map if new data is available, and
  615. # render the latest map received.
  616. new_nav_memory_map = None
  617. try:
  618. new_nav_memory_map = self._nav_memory_map_queue.popleft()
  619. except IndexError:
  620. # no new nav map - queue is empty
  621. pass
  622. # Rebuild the renderable map if it has changed
  623. if new_nav_memory_map is not None:
  624. cen = new_nav_memory_map.center
  625. half_size = new_nav_memory_map.size * 0.5
  626. if self._nav_memory_map_display_list is None:
  627. self._nav_memory_map_display_list = glGenLists(1)
  628. glNewList(self._nav_memory_map_display_list, GL_COMPILE)
  629. glPushMatrix()
  630. color_light_gray = (0.65, 0.65, 0.65)
  631. glColor3f(*color_light_gray)
  632. glBegin(GL_LINE_STRIP)
  633. glVertex3f(cen.x + half_size, cen.y + half_size, cen.z) # TL
  634. glVertex3f(cen.x + half_size, cen.y - half_size, cen.z) # TR
  635. glVertex3f(cen.x - half_size, cen.y - half_size, cen.z) # BR
  636. glVertex3f(cen.x - half_size, cen.y + half_size, cen.z) # BL
  637. glVertex3f(cen.x + half_size, cen.y + half_size,
  638. cen.z) # TL (close loop)
  639. glEnd()
  640. def color_for_content(content):
  641. nct = nav_memory_map.NodeContentTypes
  642. colors = {nct.Unknown.id: (0.3, 0.3, 0.3), # dark gray
  643. nct.ClearOfObstacle.id: (0.0, 1.0, 0.0), # green
  644. nct.ClearOfCliff.id: (0.0, 0.5, 0.0), # dark green
  645. nct.ObstacleCube.id: (1.0, 0.0, 0.0), # red
  646. nct.ObstacleCharger.id: (1.0, 0.5, 0.0), # orange
  647. nct.Cliff.id: (0.0, 0.0, 0.0), # black
  648. nct.VisionBorder.id: (1.0, 1.0, 0.0) # yellow
  649. }
  650. col = colors.get(content.id)
  651. if col is None:
  652. logger.error("Unhandled content type %s" % str(content))
  653. col = (1.0, 1.0, 1.0) # white
  654. return col
  655. fill_z = cen.z - 0.4
  656. def _recursive_draw(grid_node: nav_memory_map.NavMemoryMapGridNode):
  657. if grid_node.children is not None:
  658. for child in grid_node.children:
  659. _recursive_draw(child)
  660. else:
  661. # leaf node - render as a quad
  662. map_alpha = 0.5
  663. cen = grid_node.center
  664. half_size = grid_node.size * 0.5
  665. # Draw outline
  666. glColor4f(*color_light_gray, 1.0) # fully opaque
  667. glBegin(GL_LINE_STRIP)
  668. glVertex3f(cen.x + half_size, cen.y + half_size, cen.z)
  669. glVertex3f(cen.x + half_size, cen.y - half_size, cen.z)
  670. glVertex3f(cen.x - half_size, cen.y - half_size, cen.z)
  671. glVertex3f(cen.x - half_size, cen.y + half_size, cen.z)
  672. glVertex3f(cen.x + half_size, cen.y + half_size, cen.z)
  673. glEnd()
  674. # Draw filled contents
  675. glColor4f(*color_for_content(grid_node.content), map_alpha)
  676. glBegin(GL_TRIANGLE_STRIP)
  677. glVertex3f(cen.x + half_size, cen.y + half_size, fill_z)
  678. glVertex3f(cen.x + half_size, cen.y - half_size, fill_z)
  679. glVertex3f(cen.x - half_size, cen.y + half_size, fill_z)
  680. glVertex3f(cen.x - half_size, cen.y - half_size, fill_z)
  681. glEnd()
  682. _recursive_draw(new_nav_memory_map.root_node)
  683. glPopMatrix()
  684. glEndList()
  685. else:
  686. # The source data hasn't changed - keep using the same call list
  687. pass
  688. if self._nav_memory_map_display_list is not None:
  689. glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
  690. glEnable(GL_BLEND)
  691. glPushMatrix()
  692. glCallList(self._nav_memory_map_display_list)
  693. glPopMatrix()
  694. def _draw_cozmo(self, robot_frame):
  695. if self.cozmo_object is None:
  696. return
  697. robot_pose = robot_frame.pose
  698. robot_head_angle = robot_frame.head_angle
  699. robot_lift_position = robot_frame.lift_position
  700. # Angle of the lift in the object's initial default pose.
  701. LIFT_ANGLE_IN_DEFAULT_POSE = -11.36
  702. robot_matrix = robot_pose.to_matrix()
  703. head_angle = robot_head_angle.degrees
  704. # Get the angle of Cozmo's lift for rendering - we subtract the angle
  705. # of the lift in the default pose in the object, and apply the inverse
  706. # rotation
  707. lift_angle = -(robot_lift_position.angle.degrees - LIFT_ANGLE_IN_DEFAULT_POSE)
  708. glPushMatrix()
  709. glEnable(GL_LIGHTING)
  710. glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
  711. glEnable(GL_BLEND)
  712. glMultMatrixf(robot_matrix.in_row_order)
  713. robot_scale_amt = 10.0 # cm to mm
  714. glScalef(robot_scale_amt, robot_scale_amt, robot_scale_amt)
  715. glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
  716. # Pivot offset for where the fork rotates around itself
  717. FORK_PIVOT_X = 3.0
  718. FORK_PIVOT_Z = 3.4
  719. # Offset for the axel that the upper arm rotates around.
  720. UPPER_ARM_PIVOT_X = -3.73
  721. UPPER_ARM_PIVOT_Z = 4.47
  722. # Offset for the axel that the lower arm rotates around.
  723. LOWER_ARM_PIVOT_X = -3.74
  724. LOWER_ARM_PIVOT_Z = 3.27
  725. # Offset for the pivot that the head rotates around.
  726. HEAD_PIVOT_X = -1.1
  727. HEAD_PIVOT_Z = 4.75
  728. # Render the static body meshes - first the main body:
  729. glCallList(self.cozmo_object.meshes["body_geo"])
  730. # Render the left treads and wheels
  731. glCallList(self.cozmo_object.meshes["trackBase_L_geo"])
  732. glCallList(self.cozmo_object.meshes["wheel_BL_geo"])
  733. glCallList(self.cozmo_object.meshes["wheel_FL_geo"])
  734. glCallList(self.cozmo_object.meshes["tracks_L_geo"])
  735. # Render the right treads and wheels
  736. glCallList(self.cozmo_object.meshes["trackBase_R_geo"])
  737. glCallList(self.cozmo_object.meshes["wheel_BR_geo"])
  738. glCallList(self.cozmo_object.meshes["wheel_FR_geo"])
  739. glCallList(self.cozmo_object.meshes["tracks_R_geo"])
  740. # Render the fork at the front (but not the arms)
  741. glPushMatrix()
  742. # The fork rotates first around upper arm (to get it to the correct position).
  743. glTranslatef(UPPER_ARM_PIVOT_X, 0.0, UPPER_ARM_PIVOT_Z)
  744. glRotatef(lift_angle, 0, 1, 0)
  745. glTranslatef(-UPPER_ARM_PIVOT_X, 0.0, -UPPER_ARM_PIVOT_Z)
  746. # The fork then rotates back around itself as it always hangs vertically.
  747. glTranslatef(FORK_PIVOT_X, 0.0, FORK_PIVOT_Z)
  748. glRotatef(-lift_angle, 0, 1, 0)
  749. glTranslatef(-FORK_PIVOT_X, 0.0, -FORK_PIVOT_Z)
  750. # Render
  751. glCallList(self.cozmo_object.meshes["fork_geo"])
  752. glPopMatrix()
  753. # Render the upper arms:
  754. glPushMatrix()
  755. # Rotate the upper arms around the upper arm joint
  756. glTranslatef(UPPER_ARM_PIVOT_X, 0.0, UPPER_ARM_PIVOT_Z)
  757. glRotatef(lift_angle, 0, 1, 0)
  758. glTranslatef(-UPPER_ARM_PIVOT_X, 0.0, -UPPER_ARM_PIVOT_Z)
  759. # Render
  760. glCallList(self.cozmo_object.meshes["uprArm_L_geo"])
  761. glCallList(self.cozmo_object.meshes["uprArm_geo"])
  762. glPopMatrix()
  763. # Render the lower arms:
  764. glPushMatrix()
  765. # Rotate the lower arms around the lower arm joint
  766. glTranslatef(LOWER_ARM_PIVOT_X, 0.0, LOWER_ARM_PIVOT_Z)
  767. glRotatef(lift_angle, 0, 1, 0)
  768. glTranslatef(-LOWER_ARM_PIVOT_X, 0.0, -LOWER_ARM_PIVOT_Z)
  769. # Render
  770. glCallList(self.cozmo_object.meshes["lwrArm_L_geo"])
  771. glCallList(self.cozmo_object.meshes["lwrArm_R_geo"])
  772. glPopMatrix()
  773. # Render the head:
  774. glPushMatrix()
  775. # Rotate the head around the pivot
  776. glTranslatef(HEAD_PIVOT_X, 0.0, HEAD_PIVOT_Z)
  777. glRotatef(-head_angle, 0, 1, 0)
  778. glTranslatef(-HEAD_PIVOT_X, 0.0, -HEAD_PIVOT_Z)
  779. # Render all of the head meshes
  780. glCallList(self.cozmo_object.meshes["head_geo"])
  781. # Screen
  782. glCallList(self.cozmo_object.meshes["backScreen_mat"])
  783. glCallList(self.cozmo_object.meshes["screenEdge_geo"])
  784. glCallList(self.cozmo_object.meshes["overscan_1_geo"])
  785. # Eyes
  786. glCallList(self.cozmo_object.meshes["eye_L_geo"])
  787. glCallList(self.cozmo_object.meshes["eye_R_geo"])
  788. # Eyelids
  789. glCallList(self.cozmo_object.meshes["eyeLid_R_top_geo"])
  790. glCallList(self.cozmo_object.meshes["eyeLid_L_top_geo"])
  791. glCallList(self.cozmo_object.meshes["eyeLid_L_btm_geo"])
  792. glCallList(self.cozmo_object.meshes["eyeLid_R_btm_geo"])
  793. # Face cover (drawn last as it's translucent):
  794. glCallList(self.cozmo_object.meshes["front_Screen_geo"])
  795. glPopMatrix()
  796. glDisable(GL_LIGHTING)
  797. glPopMatrix()
  798. def _draw_unit_cube(self, color, draw_solid):
  799. glColor(color)
  800. if draw_solid:
  801. ambient_color = [color[0]*0.1, color[1]*0.1, color[2]*0.1, 1.0]
  802. else:
  803. ambient_color = color
  804. glMaterialfv(GL_FRONT, GL_AMBIENT, ambient_color)
  805. glMaterialfv(GL_FRONT, GL_DIFFUSE, color)
  806. glMaterialfv(GL_FRONT, GL_SPECULAR, color)
  807. glMaterialfv(GL_FRONT, GL_SHININESS, 10.0);
  808. if draw_solid:
  809. glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
  810. else:
  811. glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
  812. glCallList(self.unit_cube)
  813. def _display_3d_view(self, window):
  814. glutSetWindow(window.gl_window)
  815. # Clear the screen and the depth buffer
  816. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
  817. # Set up the projection matrix
  818. glMatrixMode(GL_PROJECTION)
  819. glLoadIdentity()
  820. fov = 45.0
  821. aspect_ratio = window.width / window.height
  822. near_clip_plane = 1.0
  823. far_clip_plane = 1000.0
  824. gluPerspective(fov, aspect_ratio, near_clip_plane, far_clip_plane)
  825. # Switch to model matrix for rendering everything
  826. glMatrixMode(GL_MODELVIEW)
  827. glLoadIdentity()
  828. # Add a light near the origin
  829. light_ambient = [1.0, 1.0, 1.0, 1.0]
  830. light_diffuse = [1.0, 1.0, 1.0, 1.0]
  831. light_specular = [1.0, 1.0, 1.0, 1.0]
  832. glLightfv(GL_LIGHT0, GL_AMBIENT, light_ambient)
  833. glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse)
  834. glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular)
  835. light_pos = [0, 20, 10, 1]
  836. glLightfv(GL_LIGHT0, GL_POSITION, light_pos)
  837. glEnable(GL_LIGHT0)
  838. glScalef(0.1, 0.1, 0.1) # mm to cm
  839. # Orient the camera
  840. self._calculate_camera_pos()
  841. gluLookAt(*self._camera_pos.x_y_z,
  842. *self._camera_look_at.x_y_z,
  843. *self._camera_up.x_y_z)
  844. # Update the latest world frame if there is a new one available
  845. try:
  846. world_frame = self._world_frame_queue.popleft() # type: WorldRenderFrame
  847. self._latest_world_frame = world_frame
  848. except IndexError:
  849. world_frame = self._latest_world_frame
  850. pass
  851. if world_frame is not None:
  852. glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
  853. glEnable(GL_LIGHTING)
  854. glEnable(GL_NORMALIZE) # to re-scale scaled normals
  855. robot_frame = world_frame.robot_frame
  856. robot_pose = robot_frame.pose
  857. # Render the cubes
  858. for i in range(3):
  859. cube_obj = self.cube_objects[i]
  860. cube_frame = world_frame.cube_frames[i]
  861. if cube_frame is None:
  862. continue
  863. cube_pose = cube_frame.pose
  864. if cube_pose is not None and cube_pose.is_comparable(robot_pose):
  865. glPushMatrix()
  866. # TODO if cube_pose.is_accurate is False, render half-translucent?
  867. # (This would require using a shader, or having duplicate objects)
  868. cube_matrix = cube_pose.to_matrix()
  869. glMultMatrixf(cube_matrix.in_row_order)
  870. # Cube is drawn slightly larger than the 10mm to 1 cm scale, as the model looks small otherwise
  871. cube_scale_amt = 10.7
  872. glScalef(cube_scale_amt, cube_scale_amt, cube_scale_amt)
  873. cube_obj.draw_all()
  874. glPopMatrix()
  875. glBindTexture(GL_TEXTURE_2D, 0)
  876. for face in world_frame.face_frames:
  877. face_pose = face.pose
  878. if face_pose is not None and face_pose.is_comparable(robot_pose):
  879. glPushMatrix()
  880. face_matrix = face_pose.to_matrix()
  881. glMultMatrixf(face_matrix.in_row_order)
  882. # Approximate size of a head
  883. glScalef(100, 25, 100)
  884. FACE_OBJECT_COLOR = [0.5, 0.5, 0.5, 1.0]
  885. draw_solid = face.time_since_last_seen < 30
  886. self._draw_unit_cube(FACE_OBJECT_COLOR, draw_solid)
  887. glPopMatrix()
  888. for obj in world_frame.custom_object_frames:
  889. obj_pose = obj.pose
  890. if obj_pose is not None and obj_pose.is_comparable(robot_pose):
  891. glPushMatrix()
  892. obj_matrix = obj_pose.to_matrix()
  893. glMultMatrixf(obj_matrix.in_row_order)
  894. glScalef(obj.x_size_mm * 0.5,
  895. obj.y_size_mm * 0.5,
  896. obj.z_size_mm * 0.5)
  897. # Only draw solid object for observable custom objects
  898. if obj.is_fixed:
  899. # fixed objects are drawn as transparent outlined boxes to make
  900. # it clearer that they have no effect on vision.
  901. FIXED_OBJECT_COLOR = [1.0, 0.7, 0.0, 1.0]
  902. self._draw_unit_cube(FIXED_OBJECT_COLOR, False)
  903. else:
  904. CUSTOM_OBJECT_COLOR = [1.0, 0.3, 0.3, 1.0]
  905. self._draw_unit_cube(CUSTOM_OBJECT_COLOR, True)
  906. glPopMatrix()
  907. glDisable(GL_LIGHTING)
  908. self._draw_cozmo(robot_frame)
  909. if self._show_controls:
  910. self._draw_controls()
  911. # Draw the (translucent) nav map last so it's sorted correctly against opaque geometry
  912. self._draw_memory_map()
  913. glutSwapBuffers()
  914. def _draw_controls(self):
  915. try:
  916. GLUT_BITMAP_9_BY_15
  917. except NameError:
  918. pass
  919. else:
  920. self._draw_text(GLUT_BITMAP_9_BY_15, self._instructions, 10, 10)
  921. def _draw_text(self, font, input, x, y, line_height=16, r=1.0, g=1.0, b=1.0):
  922. '''Render text based on window position. The origin is in the bottom-left.'''
  923. glColor3f(r, g, b)
  924. glWindowPos2f(x,y)
  925. input_list = input.split('\n')
  926. y = y + (line_height * (len(input_list) -1))
  927. for line in input_list:
  928. glWindowPos2f(x, y)
  929. y -= line_height
  930. for ch in line:
  931. glutBitmapCharacter(font, ctypes.c_int(ord(ch)))
  932. def _display_camera_view(self, window):
  933. glutSetWindow(window.gl_window)
  934. if self._camera_view_texture is None:
  935. self._camera_view_texture = DynamicTexture()
  936. target_width = window.width
  937. target_height = window.height
  938. target_aspect = 320 / 240 # (Camera-feed resolution and aspect ratio)
  939. max_u = 1.0
  940. max_v = 1.0
  941. if (target_width / target_height) < target_aspect:
  942. target_height = target_width / target_aspect
  943. max_v *= target_height / window.height
  944. elif (target_width / target_height) > target_aspect:
  945. target_width = target_height * target_aspect
  946. max_u *= target_width / window.width
  947. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
  948. glEnable(GL_TEXTURE_2D)
  949. # Try getting a new image if one has been added
  950. image = None
  951. try:
  952. image = self._img_queue.popleft()
  953. except IndexError:
  954. # no new image - queue is empty
  955. pass
  956. if image:
  957. # There's a new image - update the texture
  958. self._camera_view_texture.update(image)
  959. else:
  960. # keep using the most recent texture
  961. self._camera_view_texture.bind()
  962. # Display the image as a tri-strip with 4 vertices
  963. glBegin(GL_TRIANGLE_STRIP)
  964. # (0,0) = Top Left, (1,1) = Bottom Right
  965. # left, bottom
  966. glTexCoord2f(0.0, 1.0)
  967. glVertex2f(-max_u, -max_v)
  968. # right, bottom
  969. glTexCoord2f(1.0, 1.0)
  970. glVertex2f(max_u, -max_v)
  971. # left, top
  972. glTexCoord2f(0.0, 0.0)
  973. glVertex2f(-max_u, max_v)
  974. # right, top
  975. glTexCoord2f(1.0, 0.0)
  976. glVertex2f(max_u, max_v)
  977. glEnd()
  978. glDisable(GL_TEXTURE_2D)
  979. glutSwapBuffers()
  980. def _display(self):
  981. try:
  982. self._display_3d_view(self.main_window)
  983. if self.viewer_window:
  984. self._display_camera_view(self.viewer_window)
  985. except KeyboardInterrupt:
  986. logger.info("_display caught KeyboardInterrupt - exitting")
  987. self._request_exit()
  988. def _key_byte_to_lower(self, key):
  989. # Convert bytes-object (representing keyboard character) to lowercase equivalent
  990. if (key >= b'A') and (key <= b'Z'):
  991. lowercase_key = ord(key) - ord(b'A') + ord(b'a')
  992. lowercase_key = bytes([lowercase_key])
  993. return lowercase_key
  994. return key
  995. def _on_key_up(self, key, x, y):
  996. key = self._key_byte_to_lower(key)
  997. self._update_modifier_keys()
  998. self._is_key_pressed[key] = False
  999. def _on_key_down(self, key, x, y):
  1000. key = self._key_byte_to_lower(key)
  1001. self._update_modifier_keys()
  1002. self._is_key_pressed[key] = True
  1003. if ord(key) == 9: # Tab
  1004. # Set Look-At point to current robot position
  1005. world_frame = self._latest_world_frame
  1006. if world_frame is not None:
  1007. robot_pos = world_frame.robot_frame.pose.position
  1008. self._camera_look_at.set_to(robot_pos)
  1009. elif ord(key) == 27: # Escape key
  1010. self._request_exit()
  1011. elif ord(key) == 72 or ord(key) == 104: # H key
  1012. self._show_controls = not self._show_controls
  1013. def _on_special_key_up(self, key, x, y):
  1014. self._update_modifier_keys()
  1015. def _on_special_key_down(self, key, x, y):
  1016. self._update_modifier_keys()
  1017. def _on_mouse_button(self, button, state, x, y):
  1018. # Don't update modifier keys- reading modifier keys is unreliable
  1019. # from _on_mouse_button (for LMB down/up), only SHIFT key seems to read there
  1020. #self._update_modifier_keys()
  1021. is_down = (state == GLUT_DOWN)
  1022. self._is_mouse_down[button] = is_down
  1023. self._mouse_pos = util.Vector2(x, y)
  1024. def _on_mouse_move_internal(self, x, y, is_active):
  1025. # is_active is True if this is not passive (i.e. a mouse button was down)
  1026. last_mouse_pos = self._mouse_pos
  1027. self._mouse_pos = util.Vector2(x, y)
  1028. if last_mouse_pos is None:
  1029. # First mouse update - ignore (we need a delta of mouse positions)
  1030. return
  1031. left_button = self._is_mouse_down.get(GLUT_LEFT_BUTTON, False)
  1032. # For laptop and other 1-button mouse users, treat 'x' key as a right mouse button too
  1033. right_button = (self._is_mouse_down.get(GLUT_RIGHT_BUTTON, False) or
  1034. self._is_key_pressed.get(b'x', False))
  1035. MOUSE_SPEED_SCALAR = 1.0 # general scalar for all mouse movement sensitivity
  1036. MOUSE_ROTATE_SCALAR = 0.025 # additional scalar for rotation sensitivity
  1037. mouse_delta = (self._mouse_pos - last_mouse_pos) * MOUSE_SPEED_SCALAR
  1038. if left_button and right_button:
  1039. # Move up/down
  1040. self._camera_look_at._z -= mouse_delta.y
  1041. elif right_button:
  1042. # Move forward/back and left/right
  1043. pitch = self._camera_pitch
  1044. yaw = self._camera_yaw
  1045. camera_offset = util.Vector3(math.cos(yaw), math.sin(yaw), math.sin(pitch))
  1046. heading = math.atan2(camera_offset.y, camera_offset.x)
  1047. half_pi = math.pi * 0.5
  1048. self._camera_look_at._x += mouse_delta.x * math.cos(heading + half_pi)
  1049. self._camera_look_at._y += mouse_delta.x * math.sin(heading + half_pi)
  1050. self._camera_look_at._x += mouse_delta.y * math.cos(heading)
  1051. self._camera_look_at._y += mouse_delta.y * math.sin(heading)
  1052. elif left_button:
  1053. if self._is_key_pressed.get(b'z', False):
  1054. # Zoom in/out
  1055. self._camera_distance = max(0.1, self._camera_distance + mouse_delta.y)
  1056. else:
  1057. # Adjust the Camera pitch and yaw
  1058. self._camera_pitch = (self._camera_pitch - (mouse_delta.y * MOUSE_ROTATE_SCALAR))
  1059. self._camera_yaw = (self._camera_yaw + (mouse_delta.x * MOUSE_ROTATE_SCALAR)) % (2.0 * math.pi)
  1060. # Clamp pitch to slightyly less than pi/2 to avoid lock/errors at full up/down
  1061. max_rotation = math.pi * 0.49
  1062. self._camera_pitch = max(-max_rotation, min(max_rotation, self._camera_pitch))
  1063. def _on_mouse_move(self, x, y):
  1064. # Mouse movement when at least one button down
  1065. self._on_mouse_move_internal(x, y, True)
  1066. def _on_mouse_move_passive(self, x, y):
  1067. # Mouse movement when no button down
  1068. self._on_mouse_move_internal(x, y, False)
  1069. def init_display(self):
  1070. glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH)
  1071. self.main_window.init_display()
  1072. glutDisplayFunc(self._display) # Note: both windows call the same DisplayFunc
  1073. glutKeyboardFunc(self._on_key_down)
  1074. glutSpecialFunc(self._on_special_key_down)
  1075. # [Keyboard/Special]Up methods aren't supported on some old GLUT implementations
  1076. has_keyboard_up = False
  1077. has_special_up = False
  1078. try:
  1079. if bool(glutKeyboardUpFunc):
  1080. glutKeyboardUpFunc(self._on_key_up)
  1081. has_keyboard_up = True
  1082. if bool(glutSpecialUpFunc):
  1083. glutSpecialUpFunc(self._on_special_key_up)
  1084. has_special_up = True
  1085. except OpenGL.error.NullFunctionError:
  1086. # Methods aren't available on this GLUT version
  1087. pass
  1088. if not has_keyboard_up or not has_special_up:
  1089. # Warn on old GLUT implementations that don't implement much of the interface.
  1090. logger.warning("Warning: Old GLUT implementation detected - keyboard remote control of Cozmo disabled."
  1091. "We recommend installing freeglut. %s", _glut_install_instructions())
  1092. self._is_keyboard_control_enabled = False
  1093. else:
  1094. self._is_keyboard_control_enabled = True
  1095. try:
  1096. GLUT_BITMAP_9_BY_15
  1097. except NameError:
  1098. logger.warning("Warning: GLUT font not detected. Help message will be unavailable.")
  1099. glutMouseFunc(self._on_mouse_button)
  1100. glutMotionFunc(self._on_mouse_move)
  1101. glutPassiveMotionFunc(self._on_mouse_move_passive)
  1102. glutIdleFunc(self._idle)
  1103. glutVisibilityFunc(self._visible)
  1104. # Load 3D objects
  1105. _cozmo_obj = LoadedObjFile("cozmo.obj")
  1106. self.cozmo_object = RenderableObject(_cozmo_obj)
  1107. # Load the cubes, reusing the same file geometry for all 3.
  1108. _cube_obj = LoadedObjFile("cube.obj")
  1109. self.cube_objects.append(RenderableObject(_cube_obj))
  1110. self.cube_objects.append(RenderableObject(_cube_obj, override_mtl=LoadMtlFile("cube2.mtl")))
  1111. self.cube_objects.append(RenderableObject(_cube_obj, override_mtl=LoadMtlFile("cube3.mtl")))
  1112. self.unit_cube = _make_unit_cube()
  1113. if self.viewer_window:
  1114. self.viewer_window.init_display()
  1115. glutDisplayFunc(self._display) # Note: both windows call the same DisplayFunc
  1116. def mainloop(self):
  1117. self.init_display()
  1118. # use a non-blocking update loop if possible to make exit conditions
  1119. # easier (not supported on all GLUT versions).
  1120. if bool(glutCheckLoop):
  1121. while not self._exit_requested:
  1122. glutCheckLoop()
  1123. else:
  1124. # This blocks until quit
  1125. glutMainLoop()
  1126. if self._exit_requested:
  1127. # Pass the keyboard interrupt on to SDK so that it can close cleanly
  1128. raise KeyboardInterrupt
  1129. async def connect(self, sdk_conn):
  1130. sdk_robot = await sdk_conn.wait_for_robot()
  1131. # Note: OpenGL and SDK are on different threads, so we deliberately don't
  1132. # store a reference to the robot here, as we should only access it from
  1133. # events called on the SDK thread (where we can then thread-safely move
  1134. # the data into OpenGL)
  1135. self._robot_state_handler = sdk_robot.world.add_event_handler(
  1136. robot.EvtRobotStateUpdated, self.on_robot_state_update)
  1137. if self.viewer_window is not None:
  1138. # Automatically enable camera stream when viewer window is used.
  1139. sdk_robot.camera.image_stream_enabled = True
  1140. self._image_handler = sdk_robot.world.add_event_handler(
  1141. world.EvtNewCameraImage, self.on_new_camera_image)
  1142. # Automatically enable streaming of the nav memory map when using the
  1143. # viewer (can be overridden by user application after connection).
  1144. sdk_robot.world.request_nav_memory_map(0.5)
  1145. self._nav_map_handler = sdk_robot.world.add_event_handler(
  1146. nav_memory_map.EvtNewNavMemoryMap, self.on_new_nav_memory_map)
  1147. def disconnect(self):
  1148. """Called from the SDK when the program is complete and it's time to exit."""
  1149. if self._image_handler:
  1150. self._image_handler.disable()
  1151. self._image_handler = None
  1152. if self._nav_map_handler:
  1153. self._nav_map_handler.disable()
  1154. self._nav_map_handler = None
  1155. if self._robot_state_handler:
  1156. self._robot_state_handler.disable()
  1157. self._robot_state_handler = None
  1158. if not self._exit_requested:
  1159. self._request_exit()
  1160. def _update_robot_remote_control(self, robot):
  1161. # Called on SDK thread, for controlling robot from input intents
  1162. # pushed from the OpenGL thread.
  1163. try:
  1164. input_intents = self._input_intent_queue.popleft() # type: RobotControlIntents
  1165. except IndexError:
  1166. # no new input intents - do nothing
  1167. return
  1168. # Track last-used intents so that we only issue motor controls
  1169. # if different from the last frame (to minimize it fighting with an SDK
  1170. # program controlling the robot):
  1171. old_intents = self._last_robot_control_intents
  1172. self._last_robot_control_intents = input_intents
  1173. if robot.is_on_charger:
  1174. # Cozmo is stuck on the charger
  1175. if input_intents.left_wheel_speed > 0 and input_intents.right_wheel_speed > 0:
  1176. # User is trying to drive forwards (off the charger) - issue an explicit drive off action
  1177. try:
  1178. # don't wait for action to complete
  1179. robot.drive_off_charger_contacts(in_parallel=True)
  1180. except RobotBusy:
  1181. # Robot is busy doing another action - try again next time we get a drive impulse
  1182. pass
  1183. if ((old_intents.left_wheel_speed != input_intents.left_wheel_speed) or
  1184. (old_intents.right_wheel_speed != input_intents.right_wheel_speed)):
  1185. robot.drive_wheel_motors(input_intents.left_wheel_speed,
  1186. input_intents.right_wheel_speed,
  1187. input_intents.left_wheel_speed * 4,
  1188. input_intents.right_wheel_speed * 4)
  1189. if (old_intents.lift_speed != input_intents.lift_speed):
  1190. robot.move_lift(input_intents.lift_speed)
  1191. if (old_intents.head_speed != input_intents.head_speed):
  1192. robot.move_head(input_intents.head_speed)
  1193. def on_robot_state_update(self, evt, *, robot, **kw):
  1194. # Called from SDK whenever the robot state is updated (so i.e. every engine tick).
  1195. # Note: This is called from the SDK thread, so only access safe things
  1196. # We can safely capture any robot and world state here, and push to OpenGL
  1197. # (main) thread via a thread-safe queue.
  1198. world_frame = WorldRenderFrame(robot)
  1199. self._world_frame_queue.append(world_frame)
  1200. # We update remote control of the robot here too as it's the one
  1201. # method that's called frequently on the SDK thread.
  1202. self._update_robot_remote_control(robot)
  1203. def on_new_camera_image(self, evt, *, image, **kw):
  1204. # Called from SDK whenever a new image is available
  1205. # Note: This is called from the SDK thread, so only access safe things:
  1206. # viewer_window will already be created, and reading width/height is safe
  1207. # (worst case it'll be a frame old, or e.g just width/height updated)
  1208. fit_size=(self.viewer_window.width, self.viewer_window.height)
  1209. annotated_image = image.annotate_image(fit_size=fit_size)
  1210. self._img_queue.append(annotated_image)
  1211. def on_new_nav_memory_map(self, evt, *, nav_memory_map, **kw):
  1212. # Called from SDK whenever a new nav memory map is available
  1213. # Note: This is called from the SDK thread, so only access safe things
  1214. self._nav_memory_map_queue.append(nav_memory_map)