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 Shader(SceneState): def __init__(self, scene, filename): super(Shader, self).__init__(scene, filename) self.path = Path(__file__).parent.parent / 'shaders' self.filename = filename self.mtime = 0 self.program = None def toJSON(self): data = super(Shader, self).toJSON() data['filename'] = self.filename return data def setAttributeArray(self, name, gltype, offset=0, count=3, stride=0): if self.enabled <= 0: raise Exception("shader must be enabled") self.program.setAttributeBuffer(name, gltype, offset, count, stride) self.program.enableAttributeArray(name) def unsetAttributeArray(self, name): if self.enabled <= 0: raise Exception("shader must be enabled") self.program.disableAttributeArray(name) def setValue(self, name, v): if self.enabled <= 0: raise Exception("shader must be enabled") self.program.setUniformValue(name, v) def setValues(self, name, *values): if self.enabled <= 0: raise Exception("shader must be enabled") self.program.setUniformValue(name, *values) def setVector2(self, name, v): if self.enabled <= 0: raise Exception("shader must be enabled") self.program.setUniformValue(name, QVector2D(*v.flat)) def setVector3(self, name, v): if self.enabled <= 0: raise Exception("shader must be enabled") self.program.setUniformValue(name, QVector3D(*v.flat)) def setVector4(self, name, v): if self.enabled <= 0: raise Exception("shader must be enabled") self.program.setUniformValue(name, QVector4D(*v.flat)) def setMatrix4x4(self, name, m): if self.enabled <= 0: raise Exception("shader must be enabled") self.program.setUniformValue(name, QMatrix4x4(*m.flat)) def setUniforms(self, uniforms): if self.enabled <= 0: raise Exception("shader must be enabled") for (name, values) in uniforms.items(): self.program.setUniformValue(name, *values) def oncleanup(self): for item in list(self.deps): item.detachShader(self) super(Shader, self).oncleanup() def createimpl(self, gl): if not self.filename: raise Exception("missing program filename") self.program = QOpenGLShaderProgram() self.mtime = max( os.stat(self.path / ('%s.vs' % self.filename)).st_mtime, os.stat(self.path / ('%s.fs' % self.filename)).st_mtime) with io.open(self.path / ('%s.vs' % self.filename), 'r') as f: self.program.addShaderFromSourceCode(QOpenGLShader.Vertex, f.read()) with io.open(self.path / ('%s.fs' % self.filename), 'r') as f: self.program.addShaderFromSourceCode(QOpenGLShader.Fragment, f.read()) if not self.program.link(): raise Exception("invalid program") def updateimpl(self, gl): if not self.filename: raise Exception("missing program filename") mtime = max( os.stat(self.path / ('%s.vs' % self.filename)).st_mtime, os.stat(self.path / ('%s.fs' % self.filename)).st_mtime) if mtime == self.mtime: return self.destroyimpl(gl) self.createimpl(gl) def destroyimpl(self, gl): self.program.removeAllShaders() # self.program.destroy() self.program = None def enableimpl(self, gl, camera, shader): self.program.bind() def disableimpl(self, gl, camera, shader): self.program.release()