class OpenGLWidget(QOpenGLWidget, QOpenGLExtraFunctions): def __init__(self, fmt: QSurfaceFormat, parent=None, *args, **kwargs): QOpenGLWidget.__init__(self, parent, *args, **kwargs) QOpenGLExtraFunctions.__init__(self, *args, **kwargs) self.width, self.height = 1280, 720 self.program = QOpenGLShaderProgram() self.vbo = QOpenGLBuffer(QOpenGLBuffer.VertexBuffer) self.ebo = QOpenGLBuffer(QOpenGLBuffer.IndexBuffer) self.vao = QOpenGLVertexArrayObject() self.model_loc = None self.projection_loc = None self.camera_loc = None self.attrib_loc = None self.shape = Cube() self.models = [] self.model = None self.projection = None self.camera = Camera() self.last_pos = QPoint(self.width / 2.0, self.height / 2.0) self.setFormat(fmt) self.context = QOpenGLContext(self) if not self.context.create(): raise RuntimeError("Unable to create GL context") self.timer = QTimer() self.timer.timeout.connect(self.update) self.timer.start(1000.0/FPS) ### QtGL ### def initializeGL(self) -> None: self.context.aboutToBeDestroyed.connect(self.cleanup) self.initializeOpenGLFunctions() self.build_shaders() self.create_vbo() self.glClearColor(0.2, 0.0, 0.2, 0.5) def paintGL(self) -> None: self.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT) self.glEnable(gl.GL_DEPTH_TEST) self.camera.move() self.render() def resizeGL(self, width: int, height: int) -> None: self.width, self.height = width, height self.glViewport(0, 0, width, height) self.projection = pyrr.matrix44.create_perspective_projection_matrix(45, width/height, 0.1, 150) def render(self) -> None: self.program.bind() vao_binder = QOpenGLVertexArrayObject.Binder(self.vao) # TODO: switch to Qt functions (doesn't work) gl.glUniformMatrix4fv(self.projection_loc, 1, gl.GL_FALSE, self.projection) view = self.camera.get_view_matrix() gl.glUniformMatrix4fv(self.camera_loc, 1, gl.GL_FALSE, view) for model in self.models: # TODO: move to entity class ? rotation = pyrr.matrix44.create_from_axis_rotation( pyrr.vector3.create_from_matrix44_translation(model), time.time(), ) scale = pyrr.matrix44.create_from_scale( pyrr.vector3.create_from_matrix44_translation(model) * 0.1, ) rotation = pyrr.matrix44.multiply(scale, rotation) model = pyrr.matrix44.multiply(rotation, model) orbit = pyrr.Matrix44.from_y_rotation(-0.1*time.time()) model = pyrr.matrix44.multiply(model, orbit) gl.glUniformMatrix4fv(self.model_loc, 1, gl.GL_FALSE, model) self.glDrawElements(gl.GL_TRIANGLES, len(self.shape.indices), gl.GL_UNSIGNED_INT, VoidPtr(0)) self.program.release() vao_binder = None ### Helpers ### def build_shaders(self) -> None: if not self.program.addShaderFromSourceFile(QOpenGLShader.Vertex, "shaders/vertex_shader.glsl"): raise FileNotFoundError("Unable to load vertex shader") if not self.program.addShaderFromSourceFile(QOpenGLShader.Fragment, "shaders/fragment_shader.glsl"): raise FileNotFoundError("Unable to load fragment shader") if not self.program.link(): raise RuntimeError("Unable to link shader program") def create_vbo(self) -> None: self.program.bind() # suspicious behaviour ? self.vao.create() vao_binder = QOpenGLVertexArrayObject.Binder(self.vao) self.vbo.create() self.vbo.bind() self.vbo.allocate(self.shape.vertices, self.shape.vertices.nbytes) self.attrib_loc = self.program.attributeLocation("a_position") self.model_loc = self.program.uniformLocation("model") self.projection_loc = self.program.uniformLocation("projection") self.camera_loc = self.program.uniformLocation("camera") for i in range(150): x, y, z = random.normalvariate(0, 15), random.normalvariate(0, 15), random.normalvariate(0, 15) self.models.append(pyrr.matrix44.create_from_translation(pyrr.Vector3([x, y, z]))) self.ebo.create() self.ebo.bind() self.ebo.allocate(self.shape.indices, self.shape.indices.nbytes) float_size = ctypes.sizeof(ctypes.c_float) # (4) null = VoidPtr(0) pointer = VoidPtr(3 * float_size) self.glEnableVertexAttribArray(0) self.glVertexAttribPointer( 0, 3, gl.GL_FLOAT, gl.GL_FALSE, 6 * float_size, null ) self.glEnableVertexAttribArray(1) self.glVertexAttribPointer( 1, 3, gl.GL_FLOAT, gl.GL_FALSE, 6 * float_size, pointer ) self.vao.release() self.vbo.release() self.ebo.release() self.program.release() vao_binder = None ### Events ### def keyPressEvent(self, event): """W: 0x57, A: 0x41, S: 0x53, D: 0x44, space: 0x20, shift: 0x01000020""" # TODO: make keymap if event.key() == int(0x57): self.camera.keyboard_press("FORWARD") elif event.key() == int(0x53): self.camera.keyboard_press("BACKWARD") if event.key() == int(0x41): self.camera.keyboard_press("LEFT") elif event.key() == int(0x44): self.camera.keyboard_press("RIGHT") if event.key() == int(0x20): self.camera.keyboard_press("UP") elif event.key() == int(0x01000020): self.camera.keyboard_press("DOWN") def keyReleaseEvent(self, event): if event.key() == int(0x57): self.camera.keyboard_release("FORWARD") elif event.key() == int(0x53): self.camera.keyboard_release("BACKWARD") if event.key() == int(0x41): self.camera.keyboard_release("LEFT") elif event.key() == int(0x44): self.camera.keyboard_release("RIGHT") if event.key() == int(0x20): self.camera.keyboard_release("UP") elif event.key() == int(0x01000020): self.camera.keyboard_release("DOWN") def mousePressEvent(self, event): self.last_pos = QPoint(event.pos()) event.accept() def mouseMoveEvent(self, event): dx = event.x() - self.last_pos.x() dy = event.y() - self.last_pos.y() if event.buttons(): self.camera.mouse_movement(float(dx), float(dy)) event.accept() self.last_pos = QPoint(event.pos()) def wheelEvent(self, event): if event.type() in (Qt.ScrollBegin, Qt.ScrollEnd): # FIXME: doesn't seem to work, true divide err return degrees = event.delta() / 8 steps = degrees / 15 event.accept() self.camera.scroll_movement(steps) ### Slots ### @Slot() def cleanup(self): if not self.makeCurrent(): raise Exception("Could not make context current") self.vbo.destroy() self.program.bind() self.program.removeAllShaders() self.program.release() del self.program self.program = None self.doneCurrent()
class Viewport(View, QOpenGLWidget): def __init__(self, scene, updateFpsDisplay, renderSettings=Rendersettings(), parent=None): View.__init__(self, scene) QOpenGLWidget.__init__(self, parent) self.renderImage = QImage() self.renderSettings = renderSettings self.updateFpsDisplay = updateFpsDisplay self.shaderProgram = QOpenGLShaderProgram() self.viewPortShaderProgram = QOpenGLShaderProgram() self.lightShaderProgram = QOpenGLShaderProgram() self.eyeLoc = QVector3D(0.0, 0.0, 5.0) self.pressLoc = QVector2D() self.isRendering = False self.viewportPlane = ViewportPlane() self.viewportTexture = None self.timer = QTimer() self.timer.setSingleShot(True) self.timer.timeout.connect(self.checkRender) self.scene.registerView(self, [ UpdateType.MATERIAL_CHANGE, UpdateType.MATERIAL_CREATE, UpdateType.MATERIAL_DELETE, UpdateType.OBJECT_CREATE, UpdateType.OBJECT_DELETE, UpdateType.OBJECT_TRANSFORM, UpdateType.CAMERA_CHANGE, UpdateType.LIGHT_CHANGE, UpdateType.SCENE_LOAD ]) self.timestamps = None self.fpsWindow = 10 self.renderer = None self.renderStartTime = None def initializeGL(self): self.glFunctions = self.context().functions() self.glFunctions.glEnable(GL.GL_DEPTH_TEST) self.glFunctions.glEnable(GL.GL_CULL_FACE) self.glFunctions.glClearColor(0.15, 0.25, 0.3, 1.0) vertSuccess = self.shaderProgram.addShaderFromSourceFile( QOpenGLShader.Vertex, "editor/ui/shaders/vertex.vert") fragSuccess = self.shaderProgram.addShaderFromSourceFile( QOpenGLShader.Fragment, "editor/ui/shaders/fragment.frag") print("Vertex shader compilation {}".format( "successful" if vertSuccess else "failed")) print("Fragment shader compilation {}".format( "successful" if fragSuccess else "failed")) print("Program linking {}".format( "successful" if self.shaderProgram.link() else "failed")) vertSuccess = self.viewPortShaderProgram.addShaderFromSourceFile( QOpenGLShader.Vertex, "editor/ui/shaders/viewportvertex.vert") fragSuccess = self.viewPortShaderProgram.addShaderFromSourceFile( QOpenGLShader.Fragment, "editor/ui/shaders/viewportfragment.frag") print("Viewport vertex shader compilation {}".format( "successful" if vertSuccess else "failed")) print("Viewport fragment shader compilation {}".format( "successful" if fragSuccess else "failed")) print("Viewport program linking {}".format( "successful" if self.viewPortShaderProgram.link() else "failed")) vertSuccess = self.lightShaderProgram.addShaderFromSourceFile( QOpenGLShader.Vertex, "editor/ui/shaders/vertex.vert") fragSuccess = self.lightShaderProgram.addShaderFromSourceFile( QOpenGLShader.Fragment, "editor/ui/shaders/lightfragment.frag") print("Light vertex shader compilation {}".format( "successful" if vertSuccess else "failed")) print("Light fragment shader compilation {}".format( "successful" if fragSuccess else "failed")) print("Light program linking {}".format( "successful" if self.viewPortShaderProgram.link() else "failed")) self.shaderProgram.release() self.viewPortShaderProgram.release() # Call all drawable Objects initGL for c in drawableClasses: if isinstance(c, ViewportPlane): c.initGL(self.viewPortShaderProgram) else: c.initGL(self.shaderProgram) def resizeGL(self, w, h): self.scene.getCamera().setAspect(w, h) if self.viewportTexture and self.context().isValid(): self.viewportTexture.destroy() if self.context().isValid(): self.renderImage = QImage(w, h, QImage.Format_RGB32) self.renderImage.fill(Qt.black) self.viewportTexture = QOpenGLTexture(QOpenGLTexture.Target2D) self.viewportTexture.setSize(w, h) self.viewportTexture.setBorderColor(0, 0, 0, 255) self.viewportTexture.setWrapMode(QOpenGLTexture.ClampToBorder) self.viewportTexture.setMinificationFilter(QOpenGLTexture.Nearest) self.viewportTexture.setMagnificationFilter(QOpenGLTexture.Nearest) self.viewportTexture.setData(self.renderImage, QOpenGLTexture.DontGenerateMipMaps) self.updateRenderResolution(w, h) def paintGL(self): self.glFunctions.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT) if not self.isRendering: self.shaderProgram.bind() self.shaderProgram.setUniformValue( "eyePos", self.scene.getCamera().getTranslation()) self.shaderProgram.release() for obj in self.scene.getObjectIterator(): if isinstance(obj, Light): obj.draw(self.lightShaderProgram, self.glFunctions) else: obj.draw(self.shaderProgram, self.glFunctions) else: self.viewportTexture.bind() self.viewportPlane.draw(self.viewPortShaderProgram, self.glFunctions) self.viewportTexture.release() def mousePressEvent(self, e): self.pressLoc = QVector2D(e.localPos()) super().mousePressEvent(e) def mouseMoveEvent(self, e): if e.buttons() == Qt.LeftButton: currPos = QVector2D(e.localPos()) diff = currPos - self.pressLoc diff *= 0.5 self.pressLoc = currPos self.scene.getCamera().rotate(diff.x(), diff.y()) def wheelEvent(self, e): if e.modifiers() == Qt.ShiftModifier: self.scene.getCamera().zoom(-e.angleDelta().y() / 45.0) else: self.scene.getCamera().track(-e.angleDelta().y() / 600.0) def notify(self, updateType): if updateType == UpdateType.SCENE_LOAD: self.scene.getCamera().setAspect(self.width(), self.height()) if self.isRendering: if updateType == UpdateType.CAMERA_CHANGE: self.cameraToRenderer() elif updateType in [ UpdateType.OBJECT_TRANSFORM, UpdateType.OBJECT_CREATE, UpdateType.OBJECT_DELETE, UpdateType.MATERIAL_CREATE, UpdateType.MATERIAL_DELETE, UpdateType.MATERIAL_CHANGE, UpdateType.LIGHT_CHANGE ]: self.renderer.stop() self.renderer.clearMaterials() self.renderer.clearObjects() for mat in self.scene.getMaterialIterator(): self.materialToRenderer(mat) for obj in self.scene.getObjectIterator(): self.sceneObjectToRenderer(obj) self.cameraToRenderer() self.renderer.start() self.timestamps = [[time.time(), 0]] self.renderStartTime = time.time() self.update() def startRender(self): if self.isRendering: return self.isRendering = True self.renderer = PyRenderer(self.width(), self.height()) self.renderer.setRenderSettings(self.renderSettings) self.timestamps = [[time.time(), 0]] self.cameraToRenderer() self.update() for mat in self.scene.getMaterialIterator(): self.materialToRenderer(mat) for obj in self.scene.getObjectIterator(): self.sceneObjectToRenderer(obj) self.renderer.start() self.timer.setInterval(20) self.timer.start() self.renderStartTime = time.time() def stopRender(self): if not self.isRendering: return self.timer.stop() self.isRendering = False self.renderer = None self.resizeGL(self.width(), self.height()) self.update() self.updateFpsDisplay('') def updateRenderResolution(self, width, height): if not self.isRendering: return self.renderer.setResolution(width, height) self.timestamps = [[time.time(), 0]] def checkRender(self): if not self.isRendering: return imageArray, numIterations = self.renderer.getData( self.width(), self.height()) if numIterations < 0 or imageArray.shape != (self.height(), self.width()): self.timer.start() return if self.viewportTexture and self.context().isValid(): self.viewportTexture.destroy() if self.context().isValid(): self.renderImage = QImage(self.width(), self.height(), QImage.Format_RGB32) copyArray = np.ndarray(shape=(self.height(), self.width()), dtype=np.uint32, buffer=self.renderImage.bits()) copyArray[0:, 0:] = imageArray[0:, 0:] self.viewportTexture = QOpenGLTexture(QOpenGLTexture.Target2D) self.viewportTexture.setSize(self.width(), self.height()) self.viewportTexture.setBorderColor(0, 0, 0, 255) self.viewportTexture.setWrapMode(QOpenGLTexture.ClampToBorder) self.viewportTexture.setMinificationFilter(QOpenGLTexture.Nearest) self.viewportTexture.setMagnificationFilter(QOpenGLTexture.Nearest) self.viewportTexture.setData(self.renderImage, QOpenGLTexture.DontGenerateMipMaps) if len(self.timestamps) == self.fpsWindow: self.timestamps.pop() self.timestamps.insert(0, [time.time(), numIterations]) diffs = [(self.timestamps[i][1] - self.timestamps[i + 1][1]) / (self.timestamps[i][0] - self.timestamps[i + 1][0]) for i in range(len(self.timestamps) - 1)] secs = int(time.time() - self.renderStartTime) self.updateFpsDisplay( '{: 4.1f} iterations/sec {:02.0f}:{:02.0f} min {:7d} iterations {:4d} x {:4d} pixel' .format( sum(diffs) / len(diffs), secs // 60, secs % 60, numIterations, self.width(), self.height())) self.update() self.timer.start() def closeViewport(self): self.stopRender() if self.viewportTexture and self.context().isValid(): self.viewportTexture.destroy() def cameraToRenderer(self): cam = self.scene.getCamera() worldPos = cam.getTranslation() targetPos = self.scene.getFocusObject().getTranslation() self.renderer.updateCamera( [worldPos.x(), worldPos.y(), worldPos.z()], [targetPos.x(), targetPos.y(), targetPos.z()], cam.fovY, cam.fStop, cam.focusDistance, cam.stratificationLevel) def sceneObjectToRenderer(self, sceneObject): objType = None materialId = None lightIntensity = 0.0 lightColor = [0.0, 0.0, 0.0] isVisible = True if isinstance(sceneObject, Camera): objType = SceneObjectType.CAMERA materialId = -1 if isinstance(sceneObject, Sphere): objType = SceneObjectType.SPHERE materialId = sceneObject.material.id if isinstance(sceneObject, Cube): objType = SceneObjectType.CUBE materialId = sceneObject.material.id if isinstance(sceneObject, Plane): objType = SceneObjectType.PLANE materialId = sceneObject.material.id if isinstance(sceneObject, Disc): objType = SceneObjectType.DISC materialId = sceneObject.material.id if isinstance(sceneObject, Light): objType = sceneObject.getEmitterShape() materialId = sceneObject.material.id lightIntensity = sceneObject.getIntensity() colVec = sceneObject.getColor() lightColor = [colVec.x(), colVec.y(), colVec.z()] isVisible = sceneObject.getVisibility() matrixArray = sceneObject.calculateModel().transposed().copyDataTo() self.renderer.addObject(sceneObject.id, materialId, objType, matrixArray, lightIntensity, lightColor, sceneObject.area(), isVisible) def materialToRenderer(self, material): if type(material) == DiffuseMaterial: col = material.getEditorColor() self.renderer.addDiffuseMaterial( material.id, [col.x(), col.y(), col.z()]) if type(material) == SpecularMaterial: reflectionColor = material.getReflectionColor() transmissionColor = material.getTransmissionColor() self.renderer.addSpecularMaterial(material.id, [ reflectionColor.x(), reflectionColor.y(), reflectionColor.z() ], [ transmissionColor.x(), transmissionColor.y(), transmissionColor.z() ], material.getIOR()) def setRenderSettings(self, renderSettings): self.renderSettings = renderSettings if self.isRendering: self.renderer.setRenderSettings(self.renderSettings) def getRenderSettings(self): return self.renderSettings def saveImage(self): if not self.isRendering: return saveImage = self.renderImage.mirrored(horizontal=True) fileTuple = QFileDialog.getSaveFileName( self, 'Save destination', dir="./images", filter='Images (*.png *.jpg);;All files (*)') if fileTuple[0]: filename = fileTuple[0] if '.' not in filename: filename += '.png' saveImage.save(filename, quality=100)