1# Copyright (c) 2019 Ultimaker B.V. 2# Uranium is released under the terms of the LGPLv3 or higher. 3 4import sys 5import ctypes # type: ignore 6 7from PyQt5.QtGui import QOpenGLVersionProfile, QOpenGLContext, QOpenGLFramebufferObject, QOpenGLBuffer 8from PyQt5.QtWidgets import QMessageBox 9from typing import Any, TYPE_CHECKING, cast 10 11from UM.Logger import Logger 12 13from UM.Version import Version 14from UM.View.GL.FrameBufferObject import FrameBufferObject 15from UM.View.GL.ShaderProgram import ShaderProgram 16from UM.View.GL.ShaderProgram import InvalidShaderProgramError 17from UM.View.GL.Texture import Texture 18from UM.View.GL.OpenGLContext import OpenGLContext 19from UM.i18n import i18nCatalog # To make dialogs translatable. 20i18n_catalog = i18nCatalog("uranium") 21 22import OpenGL.GL as gl 23 24if TYPE_CHECKING: 25 from UM.Mesh.MeshData import MeshData 26 27 28class OpenGL: 29 """Convenience methods for dealing with OpenGL. 30 31 This class simplifies dealing with OpenGL and different Python OpenGL bindings. It 32 mostly describes an interface that should be implemented for dealing with basic OpenGL 33 functionality using these different OpenGL bindings. Additionally, it provides singleton 34 handling. The implementation-defined subclass must be set as singleton instance as soon 35 as possible so that any calls to getInstance() return a proper object. 36 """ 37 VertexBufferProperty = "__vertex_buffer" 38 IndexBufferProperty = "__index_buffer" 39 40 class Vendor: 41 """Different OpenGL chipset vendors.""" 42 NVidia = 1 43 AMD = 2 44 Intel = 3 45 Other = 4 46 47 def __init__(self) -> None: 48 if OpenGL.__instance is not None: 49 raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__) 50 OpenGL.__instance = self 51 52 super().__init__() 53 54 profile = QOpenGLVersionProfile() 55 profile.setVersion(OpenGLContext.major_version, OpenGLContext.minor_version) 56 profile.setProfile(OpenGLContext.profile) 57 58 context = QOpenGLContext.currentContext() 59 if not context: 60 Logger.log("e", "Startup failed due to OpenGL context creation failing") 61 QMessageBox.critical(None, i18n_catalog.i18nc("@message", "Failed to Initialize OpenGL", "Could not initialize an OpenGL context. This program requires OpenGL 2.0 or higher. Please check your video card drivers.")) 62 sys.exit(1) 63 self._gl = gl 64# self._gl = context.versionFunctions(profile) # type: Any #It's actually a protected class in PyQt that depends on the implementation of your graphics card. 65 if not self._gl: 66 Logger.log("e", "Startup failed due to OpenGL initialization failing") 67 QMessageBox.critical(None, i18n_catalog.i18nc("@message", "Failed to Initialize OpenGL", "Could not initialize OpenGL. This program requires OpenGL 2.0 or higher. Please check your video card drivers.")) 68 sys.exit(1) 69 70 # It would be nice to be able to not necessarily need OpenGL FrameBuffer Object support, but 71 # due to a limitation in PyQt, currently glReadPixels or similar methods are not available. 72 # This means we can only get frame buffer contents through methods that indirectly call 73 # those methods, in this case primarily QOpenGLFrameBufferObject::toImage(), making us 74 # hard-depend on FrameBuffer Objects. 75 if not self.hasFrameBufferObjects(): 76 Logger.log("e", "Startup failed, OpenGL does not support Frame Buffer Objects") 77 QMessageBox.critical(None, i18n_catalog.i18nc("Critical OpenGL Extensions Missing", "Critical OpenGL extensions are missing. This program requires support for Framebuffer Objects. Please check your video card drivers.")) 78 sys.exit(1) 79 80# self._gl.initializeOpenGLFunctions() 81 82 self._gpu_vendor = OpenGL.Vendor.Other #type: int 83 vendor_string = self._gl.glGetString(self._gl.GL_VENDOR).decode("utf-8") 84 if vendor_string is None: 85 vendor_string = "Unknown" 86 vendor_string = vendor_string.lower() 87 if "nvidia" in vendor_string: 88 self._gpu_vendor = OpenGL.Vendor.NVidia 89 elif "amd" in vendor_string or "ati" in vendor_string: 90 self._gpu_vendor = OpenGL.Vendor.AMD 91 elif "intel" in vendor_string: 92 self._gpu_vendor = OpenGL.Vendor.Intel 93 94 self._gpu_type = "Unknown" # type: str 95 # WORKAROUND: Cura/#1117 Cura-packaging/12 96 # Some Intel GPU chipsets return a string, which is not undecodable via PyQt5. 97 # This workaround makes the code fall back to a "Unknown" renderer in these cases. 98 try: 99 self._gpu_type = self._gl.glGetString(self._gl.GL_RENDERER) 100 except UnicodeDecodeError: 101 Logger.log("e", "DecodeError while getting GL_RENDERER via glGetString!") 102 103 self._opengl_version = self._gl.glGetString(self._gl.GL_VERSION) #type: str 104 105 self._opengl_shading_language_version = Version("0.0") # type: Version 106 try: 107 self._opengl_shading_language_version = Version(self._gl.glGetString(self._gl.GL_SHADING_LANGUAGE_VERSION)) 108 except: 109 self._opengl_shading_language_version = Version("1.0") 110 111 if not self.hasFrameBufferObjects(): 112 Logger.log("w", "No frame buffer support, falling back to texture copies.") 113 114 Logger.log("d", "Initialized OpenGL subsystems.") 115 Logger.log("d", "OpenGL Version: %s", self._opengl_version) 116 Logger.log("d", "OpenGL Vendor: %s", self._gl.glGetString(self._gl.GL_VENDOR)) 117 Logger.log("d", "OpenGL Renderer: %s", self._gpu_type) 118 Logger.log("d", "GLSL Version: %s", self._opengl_shading_language_version) 119 120 def hasFrameBufferObjects(self) -> bool: 121 """Check if the current OpenGL implementation supports FrameBuffer Objects. 122 123 :return: True if FBOs are supported, False if not. 124 """ 125 return QOpenGLFramebufferObject.hasOpenGLFramebufferObjects() 126 127 def getOpenGLVersion(self) -> str: 128 """Get the current OpenGL version. 129 130 :return: Version of OpenGL 131 """ 132 return self._opengl_version 133 134 def getOpenGLShadingLanguageVersion(self) -> "Version": 135 """Get the current OpenGL shading language version. 136 137 :return: Shading language version of OpenGL 138 """ 139 return self._opengl_shading_language_version 140 141 def getGPUVendorName(self) -> str: 142 """Get the current GPU vendor name. 143 144 :return: Name of the vendor of current GPU 145 """ 146 return self._gl.glGetString(self._gl.GL_VENDOR) 147 148 def getGPUVendor(self) -> int: 149 """Get the current GPU vendor. 150 151 :return: One of the items of OpenGL.Vendor. 152 """ 153 return self._gpu_vendor 154 155 def getGPUType(self) -> str: 156 """Get a string describing the current GPU type. 157 158 This effectively should return the OpenGL renderer string. 159 """ 160 return self._gpu_type 161 162 def getBindingsObject(self) -> Any: 163 """Get the OpenGL bindings object. 164 165 This should return an object that has all supported OpenGL functions 166 as methods and additionally defines all OpenGL constants. This object 167 is used to make direct OpenGL calls so should match OpenGL as closely 168 as possible. 169 """ 170 return self._gl 171 172 def createFrameBufferObject(self, width: int, height: int) -> FrameBufferObject: 173 """Create a FrameBuffer Object. 174 175 This should return an implementation-specifc FrameBufferObject subclass. 176 """ 177 return FrameBufferObject(width, height) 178 179 def createTexture(self) -> Texture: 180 """Create a Texture Object. 181 182 This should return an implementation-specific Texture subclass. 183 """ 184 return Texture(self._gl) 185 186 def createShaderProgram(self, file_name: str) -> ShaderProgram: 187 """Create a ShaderProgram Object. 188 189 This should return an implementation-specifc ShaderProgram subclass. 190 """ 191 shader = ShaderProgram() 192 # The version_string must match the keys in shader files. 193 if OpenGLContext.isLegacyOpenGL(): 194 version_string = "" # Nothing is added to "fragment" and "vertex" 195 else: 196 version_string = "41core" 197 198 try: 199 shader.load(file_name, version = version_string) 200 except InvalidShaderProgramError: 201 # If the loading failed, it could be that there is no specific shader for this version. 202 # Try again without a version nr to get the generic one. 203 if version_string != "": 204 shader.load(file_name, version = "") 205 return shader 206 207 def createVertexBuffer(self, mesh: "MeshData", **kwargs: Any) -> QOpenGLBuffer: 208 """Create a Vertex buffer for a mesh. 209 210 This will create a vertex buffer object that is filled with the 211 vertex data of the mesh. 212 213 By default, the associated vertex buffer should be cached using a 214 custom property on the mesh. This should use the VertexBufferProperty 215 property name. 216 217 :param mesh: The mesh to create a vertex buffer for. 218 :param kwargs: Keyword arguments. 219 Possible values: 220 - force_recreate: Ignore the cached value if set and always create a new buffer. 221 """ 222 if not kwargs.get("force_recreate", False) and hasattr(mesh, OpenGL.VertexBufferProperty): 223 return getattr(mesh, OpenGL.VertexBufferProperty) 224 225 buffer = QOpenGLBuffer(QOpenGLBuffer.VertexBuffer) 226 buffer.create() 227 buffer.bind() 228 229 float_size = ctypes.sizeof(ctypes.c_float) 230 int_size = ctypes.sizeof(ctypes.c_int) 231 232 buffer_size = mesh.getVertexCount() * 3 * float_size # Vertex count * number of components * sizeof(float32) 233 if mesh.hasNormals(): 234 buffer_size += mesh.getVertexCount() * 3 * float_size # Vertex count * number of components * sizeof(float32) 235 if mesh.hasColors(): 236 buffer_size += mesh.getVertexCount() * 4 * float_size # Vertex count * number of components * sizeof(float32) 237 if mesh.hasUVCoordinates(): 238 buffer_size += mesh.getVertexCount() * 2 * float_size # Vertex count * number of components * sizeof(float32) 239 for attribute_name in mesh.attributeNames(): 240 attribute = mesh.getAttribute(attribute_name) 241 if attribute["opengl_type"] == "vector2f": 242 buffer_size += mesh.getVertexCount() * 2 * float_size 243 elif attribute["opengl_type"] == "vector4f": 244 buffer_size += mesh.getVertexCount() * 4 * float_size 245 elif attribute["opengl_type"] == "int": 246 buffer_size += mesh.getVertexCount() * int_size 247 elif attribute["opengl_type"] == "float": 248 buffer_size += mesh.getVertexCount() * float_size 249 else: 250 Logger.log( 251 "e", "Could not determine buffer size for attribute [%s] with type [%s]" % (attribute_name, attribute["opengl_type"])) 252 buffer.allocate(buffer_size) 253 254 offset = 0 255 vertices = mesh.getVerticesAsByteArray() 256 if vertices is not None: 257 buffer.write(0, vertices, len(vertices)) 258 offset += len(vertices) 259 260 if mesh.hasNormals(): 261 normals = cast(bytes, mesh.getNormalsAsByteArray()) 262 buffer.write(offset, normals, len(normals)) 263 offset += len(normals) 264 265 if mesh.hasColors(): 266 colors = cast(bytes, mesh.getColorsAsByteArray()) 267 buffer.write(offset, colors, len(colors)) 268 offset += len(colors) 269 270 if mesh.hasUVCoordinates(): 271 uvs = cast(bytes, mesh.getUVCoordinatesAsByteArray()) 272 buffer.write(offset, uvs, len(uvs)) 273 offset += len(uvs) 274 275 for attribute_name in mesh.attributeNames(): 276 attribute = mesh.getAttribute(attribute_name) 277 attribute_byte_array = attribute["value"].tostring() 278 buffer.write(offset, attribute_byte_array, len(attribute_byte_array)) 279 offset += len(attribute_byte_array) 280 281 buffer.release() 282 283 setattr(mesh, OpenGL.VertexBufferProperty, buffer) 284 return buffer 285 286 def createIndexBuffer(self, mesh: "MeshData", **kwargs: Any): 287 """Create an index buffer for a mesh. 288 289 This will create an index buffer object that is filled with the 290 index data of the mesh. 291 292 By default, the associated index buffer should be cached using a 293 custom property on the mesh. This should use the IndexBufferProperty 294 property name. 295 296 :param mesh: The mesh to create an index buffer for. 297 :param kwargs: Keyword arguments. 298 Possible values: 299 - force_recreate: Ignore the cached value if set and always create a new buffer. 300 """ 301 if not mesh.hasIndices(): 302 return None 303 304 if not kwargs.get("force_recreate", False) and hasattr(mesh, OpenGL.IndexBufferProperty): 305 return getattr(mesh, OpenGL.IndexBufferProperty) 306 307 buffer = QOpenGLBuffer(QOpenGLBuffer.IndexBuffer) 308 buffer.create() 309 buffer.bind() 310 311 data = cast(bytes, mesh.getIndicesAsByteArray()) # We check for None at the beginning of the method 312 if 'index_start' in kwargs and 'index_stop' in kwargs: 313 buffer.allocate(data[4 * kwargs['index_start']:4 * kwargs['index_stop']], 4*(kwargs['index_stop'] - kwargs['index_start'])) 314 else: 315 buffer.allocate(data, len(data)) 316 buffer.release() 317 318 setattr(mesh, OpenGL.IndexBufferProperty, buffer) 319 320 return buffer 321 322 __instance = None # type: OpenGL 323 324 @classmethod 325 def getInstance(cls, *args, **kwargs) -> "OpenGL": 326 return cls.__instance 327