diff --git a/README.md b/README.md index 529aee2..ff4c213 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,11 @@ commands on the terminal. - Create a virtual env with conda `conda create -n pyside-opengl-tuto` -- Activate your virtual env `conda actiate pyside-opengl-tuto` +- Activate your virtual env `conda activate pyside-opengl-tuto` - Install python `conda install -c conda-forge python=3` -- Install `qt` `conda install -c conda-forge qt` +- Install `pyside2` and `shiboken2` `pip install PySide2==5.11 shiboken2==5.12` - Install PyOpenGL_accelerate `conda install -c anaconda pyopengl-accelerate` @@ -46,3 +46,10 @@ The tutorials are linear in nature, so you can use it alongside with other learning ressources for OpenGL. If you feel like you can contribute to tutorials, they are always welcomed. + + +## List of Tutorials + +As stated in the description the list is progressive. + +1. [Hello Triangle](./tutorials/01-triangle/TriangleTutorial.ipynb) diff --git a/setup.py b/setup.py index 6a2e0bf..05febb0 100644 --- a/setup.py +++ b/setup.py @@ -28,8 +28,7 @@ test_suite="tests", install_requires=[ "numpy", - "pillow", - "PySide2", + "jupyter" ], classifiers=[ "Programming Language :: Python :: 3", diff --git a/tutorials/01-triangle/TriangleTutorial.ipynb b/tutorials/01-triangle/TriangleTutorial.ipynb index 0827a09..c1c3fa6 100644 --- a/tutorials/01-triangle/TriangleTutorial.ipynb +++ b/tutorials/01-triangle/TriangleTutorial.ipynb @@ -77,7 +77,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "from PySide2 import QtWidgets, QtCore, QtGui\n", @@ -126,7 +128,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "class AppWindow(QtWidgets.QMainWindow):\n", @@ -195,7 +199,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "from PySide2 import QtWidgets, QtCore, QtGui\n", @@ -257,7 +263,809 @@ "\n", "As you can see it is a fairly simple window which contains 3 sliders and an opengl widget.\n", "\n", - "Now let's create our OpenGL widget" + "Now let's create our OpenGL widget. First, let's see what objects we shall use for the widget" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "import numpy as np # facilitates interfacing with c \n", + "import os # general path manipulation\n", + "import sys # to send the exit signal if necessary\n", + "import ctypes # a must for communicating with c code under the opengl hood\n", + "\n", + "from PySide2 import QtWidgets, QtCore, QtGui\n", + "from PySide2.QtGui import QVector3D # for attribute/uniform values of type vec3 in shaders\n", + "from PySide2.QtGui import QOpenGLVertexArrayObject # the VAO in opengl jargon\n", + "from PySide2.QtGui import QOpenGLBuffer # a buffer object for storing your data\n", + "from PySide2.QtGui import QOpenGLShaderProgram # the shader program to which we can attach shaders\n", + "from PySide2.QtGui import QOpenGLShader # represents a shader\n", + "from PySide2.QtGui import QOpenGLContext # an opengl context in which a drawing occurs\n", + "from PySide2.QtGui import QMatrix4x4 # for attribute/uniform values of type mat4 in shaders\n", + "from PySide2.QtGui import QVector4D # for attribute/uniform values of type vec4 in shaders\n", + "\n", + "from PySide2.QtWidgets import QApplication # need to display error message \n", + "from PySide2.QtWidgets import QMessageBox # the box in which the message will appear\n", + "from PySide2.QtWidgets import QOpenGLWidget # the abstract class that we will inherit \n", + "# for constructing our widget\n", + "\n", + "from PySide2.QtCore import QCoreApplication\n", + "\n", + "from PySide2.shiboken2 import VoidPtr # needed for attribute pointer function" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A crucial library for accessing opengl related flags is `pyopengl`. \n", + "So we need to check if it exists if we want to do anything related to opengl in python." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "try:\n", + " from OpenGL import GL as pygl\n", + "except ImportError:\n", + " app = QApplication(sys.argv)\n", + " messageBox = QMessageBox(QMessageBox.Critical, \"OpenGL hellogl\",\n", + " \"PyOpenGL must be installed to run this example.\",\n", + " QMessageBox.Close)\n", + " messageBox.setDetailedText(\n", + " \"Run:\\npip install PyOpenGL PyOpenGL_accelerate\")\n", + " messageBox.exec_()\n", + " sys.exit(1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's see now the actual glwidget constructor." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "class TriangleGL(QOpenGLWidget):\n", + " def __init__(self, parent=None):\n", + " QOpenGLWidget.__init__(self, parent)\n", + "\n", + " # shaders etc\n", + " projectdir = os.getcwd()\n", + " self.shaders = { # notice the syntax of the used shading language\n", + " \"portableTriangle\": { # it is a little different than desktop opengl\n", + " \"fragment\": \"\"\" \n", + "uniform mediump vec4 color;\n", + "\n", + "void main(void)\n", + "{\n", + " gl_FragColor = color;\n", + "}\"\"\",\n", + " \"vertex\": \"\"\"\n", + "attribute highp vec3 aPos;\n", + "void main(void)\n", + "{\n", + " gl_Position = vec4(aPos, 1.0);\n", + "}\n", + "\n", + "\"\"\"\n", + " }\n", + " }\n", + " self.core = \"--coreprofile\" in QCoreApplication.arguments()\n", + "\n", + " # opengl data related\n", + " self.context = QOpenGLContext()\n", + " self.vao = QOpenGLVertexArrayObject()\n", + " self.vbo = QOpenGLBuffer(QOpenGLBuffer.VertexBuffer)\n", + " self.program = QOpenGLShaderProgram()\n", + "\n", + " # some vertex data for corners of triangle\n", + " # please do note that the dimension of the array is 1\n", + " # we shall specify the offset and stride for the\n", + " # vertices of the triangle\n", + "\n", + " self.vertexData = np.array(\n", + " [-0.5, -0.5, 0.0, # x, y, z\n", + " 0.5, -0.5, 0.0, # x, y, z\n", + " 0.0, 0.5, 0.0], # x, y, z\n", + " dtype=ctypes.c_float # notice the ctype for interfacing the underlaying c lib\n", + " )\n", + " # triangle color\n", + " self.triangleColor = QVector4D(0.5, 0.5, 0.0, 0.0) # yellow triangle\n", + " # notice the correspondance the vec4 of fragment shader \n", + " # and our choice here" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It is important to check the state of opengl in our machine. We can do so with the following method. Do not forget that your version of opengl has implication on the functions and the shaders you can use in your code." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + " def getGlInfo(self):\n", + " \"Get opengl info\"\n", + " info = \"\"\"\n", + " Vendor: {0}\n", + " Renderer: {1}\n", + " OpenGL Version: {2}\n", + " Shader Version: {3}\n", + " \"\"\".format(\n", + " pygl.glGetString(pygl.GL_VENDOR),\n", + " pygl.glGetString(pygl.GL_RENDERER),\n", + " pygl.glGetString(pygl.GL_VERSION),\n", + " pygl.glGetString(pygl.GL_SHADING_LANGUAGE_VERSION)\n", + " )\n", + " return info" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Since shaders are a big part of opengl. Let's see the shader related part of our glwidget." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + " def loadShader(self,\n", + " shaderName: str,\n", + " shaderType: str):\n", + " \"Load shader\"\n", + " shader = self.shaders[shaderName] # we choose the shader from available shaders\n", + " shaderSource = shader[shaderType] # we take the source of the shader\n", + " if shaderType == \"vertex\": # Notice that we specify the type of the shader in the\n", + " shader = QOpenGLShader(QOpenGLShader.Vertex) # constructor of the qt-shader object \n", + " else:\n", + " shader = QOpenGLShader(QOpenGLShader.Fragment)\n", + " #\n", + " isCompiled = shader.compileSourceCode(shaderSource) # compilation of the shader\n", + " # we can not attach the shader to program before compilation\n", + " # so it is important check if the compilation occured without error\n", + "\n", + " if isCompiled is False:\n", + " print(shader.log())\n", + " raise ValueError(\n", + " \"{0} shader {2} known as {1} is not compiled\".format(\n", + " shaderType, shaderName, shaderSource\n", + " )\n", + " )\n", + " return shader\n", + "\n", + " def loadVertexShader(self, shaderName: str): # loads vertex shader\n", + " \"load vertex shader\"\n", + " return self.loadShader(shaderName, \"vertex\")\n", + "\n", + " def loadFragmentShader(self, shaderName: str): # loads fragment shader\n", + " \"load fragment shader\"\n", + " return self.loadShader(shaderName, \"fragment\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's see the actual drawing code which corresponds to the drawing loop in an equivalent c/c++ code. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + " def paintGL(self): # Notice the uppercase of GL because this paintGL function corresponds to \n", + " \"drawing loop\" # a virtual function in c code some you need to implement this function\n", + " # if you want to draw anything \n", + " # functions that are available for our current drawing context\n", + " funcs = self.context.functions()\n", + "\n", + " # clean up what was drawn in the previous frame\n", + " funcs.glClear(pygl.GL_COLOR_BUFFER_BIT)\n", + "\n", + " # actual drawing code\n", + " vaoBinder = QOpenGLVertexArrayObject.Binder(self.vao) # we bind the vertex array object\n", + " self.program.bind() # we bind the program means we activate the program\n", + " funcs.glDrawArrays(pygl.GL_TRIANGLES, # we draw the triangle\n", + " 0,\n", + " 3)\n", + " self.program.release() # the frame is drawn so we can deactivate the program\n", + " vaoBinder = None # we can unbind the vao since again the frame is drawn" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "What if we resize the viewport" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + " def resizeGL(self, width: int, height: int):\n", + " \"Resize the viewport\"\n", + " funcs = self.context.functions() # get the functions available for our context\n", + " funcs.glViewport(0, 0, width, height)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "What if we close the opengl program, we should release the ressources." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + " def cleanUpGl(self):\n", + " \"Clean up everything\"\n", + " self.context.makeCurrent() # we first make the context we want to release current\n", + " self.vbo.destroy() # we destroy the buffer that holds the data\n", + " del self.program # we delete the shader program, thus free the memory from it\n", + " self.program = None # we change the value of the pointed reference \n", + " self.doneCurrent() # we make no context current in the current thread " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's see the most daunting part of the code that is initialization of the gl widget.\n", + "First let's see the creation of the context in which the drawing would occur." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + " def initializeGL(self):\n", + " print('gl initial')\n", + " print(self.getGlInfo())\n", + " # create context \n", + " self.context.create()\n", + " # if the close signal is given we clean up the ressources as per defined above\n", + " self.context.aboutToBeDestroyed.connect(self.cleanUpGl) " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We initialize the function that are available for the current context." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + " # initialize functions\n", + " funcs = self.context.functions() # we obtain functions for the current context\n", + " funcs.initializeOpenGLFunctions() # we initialize functions\n", + " funcs.glClearColor(1, 1, 1, 1) # the color that will fill the frame when we call the function\n", + " # for cleaning the frame in paintGL" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's see how we initialize the shaders and shader program." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + " # deal with shaders\n", + " shaderName = \"portableTriangle\"\n", + " vshader = self.loadVertexShader(shaderName)\n", + " fshader = self.loadFragmentShader(shaderName)\n", + "\n", + " # creating shader program\n", + " self.program = QOpenGLShaderProgram(self.context)\n", + " self.program.addShader(vshader) # adding vertex shader\n", + " self.program.addShader(fshader) # adding fragment shader\n", + "\n", + " # bind attribute to a location\n", + " self.program.bindAttributeLocation(\"aPos\", 0) # notice the correspondance of the\n", + " # name aPos in the vertex shader source\n", + "\n", + " # link shader program\n", + " isLinked = self.program.link()\n", + " print(\"shader program is linked: \", isLinked)\n", + " # if the program is not linked we won't have any output so\n", + " # it is important to check for it\n", + "\n", + " # bind the program == activate the program\n", + " self.program.bind()\n", + "\n", + " # specify uniform value\n", + " colorLoc = self.program.uniformLocation(\"color\") \n", + " # notice the correspondance of the\n", + " # name color in fragment shader\n", + " # we also obtain the uniform location in order to \n", + " # set value to it\n", + " self.program.setUniformValue(colorLoc,\n", + " self.triangleColor)\n", + " # notice the correspondance of the color type vec4 \n", + " # and the type of triangleColor" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's create the vao which holds the vertex structure data, and buffer which holds the data itself." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + " # create vao and vbo\n", + "\n", + " # vao\n", + " isVao = self.vao.create()\n", + " vaoBinder = QOpenGLVertexArrayObject.Binder(self.vao)\n", + "\n", + " # vbo\n", + " isVbo = self.vbo.create()\n", + " isBound = self.vbo.bind()\n", + "\n", + " # check if vao and vbo are created\n", + " print('vao created: ', isVao)\n", + " print('vbo created: ', isVbo)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let us allocate the space necessary for holding data in buffer." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + " floatSize = ctypes.sizeof(ctypes.c_float)\n", + "\n", + " # allocate space on buffer\n", + " self.vbo.allocate(self.vertexData.tobytes(), # the actual content of the data\n", + " floatSize * self.vertexData.size # the size of the data\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's see how to use vertex array object." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + " funcs.glEnableVertexAttribArray(0) \n", + " # 0 represent the location of aPos\n", + " # we know this number because it is us who bind it to that location above\n", + " nullptr = VoidPtr(0) # no idea what we do with this thing.\n", + " funcs.glVertexAttribPointer(0, # the location of aPos attribute\n", + " 3, # 3 for vec3\n", + " int(pygl.GL_FLOAT), # type of value in the coordinates\n", + " # notice that we use a flag from opengl\n", + " int(pygl.GL_FALSE), # should we normalize the coordinates\n", + " # or not\n", + " 3 * floatSize, # stride. That is when does the next vertice\n", + " # start in the array\n", + " nullptr # offset. From where the coordinates starts\n", + " # in the array, since we only have vertex coordinates \n", + " # in the array, we start from 0\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we are done with all the steps. We should release the ressources." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + " self.vbo.release()\n", + " self.program.release()\n", + " vaoBinder = None" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here is all of the method for initializing the glwidget." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + " def initializeGL(self):\n", + " print('gl initial')\n", + " print(self.getGlInfo())\n", + " # create context and make it current\n", + " self.context.create()\n", + " self.context.aboutToBeDestroyed.connect(self.cleanUpGl)\n", + " \n", + " # initialize functions\n", + " funcs = self.context.functions()\n", + " funcs.initializeOpenGLFunctions()\n", + " funcs.glClearColor(1, 1, 1, 1)\n", + "\n", + " # deal with shaders\n", + " shaderName = \"portableTriangle\"\n", + " vshader = self.loadVertexShader(shaderName)\n", + " fshader = self.loadFragmentShader(shaderName)\n", + "\n", + " # creating shader program\n", + " self.program = QOpenGLShaderProgram(self.context)\n", + " self.program.addShader(vshader) # adding vertex shader\n", + " self.program.addShader(fshader) # adding fragment shader\n", + "\n", + " # bind attribute to a location\n", + " self.program.bindAttributeLocation(\"aPos\", 0)\n", + "\n", + " # link shader program\n", + " isLinked = self.program.link()\n", + " print(\"shader program is linked: \", isLinked)\n", + "\n", + " # bind the program\n", + " self.program.bind()\n", + "\n", + " # specify uniform value\n", + " colorLoc = self.program.uniformLocation(\"color\")\n", + " self.program.setUniformValue(colorLoc,\n", + " self.triangleColor)\n", + "\n", + " # deal with vao and vbo\n", + "\n", + " # create vao and vbo\n", + "\n", + " # vao\n", + " isVao = self.vao.create()\n", + " vaoBinder = QOpenGLVertexArrayObject.Binder(self.vao)\n", + "\n", + " # vbo\n", + " isVbo = self.vbo.create()\n", + " isBound = self.vbo.bind()\n", + "\n", + " # check if vao and vbo are created\n", + " print('vao created: ', isVao)\n", + " print('vbo created: ', isVbo)\n", + "\n", + " floatSize = ctypes.sizeof(ctypes.c_float)\n", + "\n", + " # allocate space on buffer\n", + " self.vbo.allocate(self.vertexData.tobytes(),\n", + " floatSize * self.vertexData.size)\n", + " funcs.glEnableVertexAttribArray(0)\n", + " nullptr = VoidPtr(0)\n", + " funcs.glVertexAttribPointer(0,\n", + " 3,\n", + " int(pygl.GL_FLOAT),\n", + " int(pygl.GL_FALSE),\n", + " 3 * floatSize,\n", + " nullptr)\n", + " self.vbo.release()\n", + " self.program.release()\n", + " vaoBinder = None" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's see all of our class which represent the widget." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "class TriangleGL(QOpenGLWidget):\n", + " def __init__(self, parent=None):\n", + " QOpenGLWidget.__init__(self, parent)\n", + "\n", + " # shaders etc\n", + " projectdir = os.getcwd()\n", + " self.shaders = {\n", + " \"portableTriangle\": {\n", + " \"fragment\": \"\"\"\n", + "uniform mediump vec4 color;\n", + "\n", + "void main(void)\n", + "{\n", + " gl_FragColor = color;\n", + "}\"\"\",\n", + " \"vertex\": \"\"\"\n", + "attribute highp vec3 aPos;\n", + "void main(void)\n", + "{\n", + " gl_Position = vec4(aPos, 1.0);\n", + "}\n", + "\n", + "\"\"\",\n", + "\n", + " }\n", + " }\n", + " self.core = \"--coreprofile\" in QCoreApplication.arguments()\n", + "\n", + " # opengl data related\n", + " self.context = QOpenGLContext()\n", + " self.vao = QOpenGLVertexArrayObject()\n", + " self.vbo = QOpenGLBuffer(QOpenGLBuffer.VertexBuffer)\n", + " self.program = QOpenGLShaderProgram()\n", + "\n", + " # some vertex data for corners of triangle\n", + " self.vertexData = np.array(\n", + " [-0.5, -0.5, 0.0, # x, y, z\n", + " 0.5, -0.5, 0.0, # x, y, z\n", + " 0.0, 0.5, 0.0], # x, y, z\n", + " dtype=ctypes.c_float\n", + " )\n", + " # triangle color\n", + " self.triangleColor = QVector4D(0.5, 0.5, 0.0, 0.0) # yellow triangle\n", + " # notice the correspondance the vec4 of fragment shader \n", + " # and our choice here\n", + "\n", + " def loadShader(self,\n", + " shaderName: str,\n", + " shaderType: str):\n", + " \"Load shader\"\n", + " shader = self.shaders[shaderName]\n", + " shaderSource = shader[shaderType]\n", + " if shaderType == \"vertex\":\n", + " shader = QOpenGLShader(QOpenGLShader.Vertex)\n", + " else:\n", + " shader = QOpenGLShader(QOpenGLShader.Fragment)\n", + " #\n", + " isCompiled = shader.compileSourceCode(shaderSource)\n", + "\n", + " if isCompiled is False:\n", + " print(shader.log())\n", + " raise ValueError(\n", + " \"{0} shader {2} known as {1} is not compiled\".format(\n", + " shaderType, shaderName, shaderSource\n", + " )\n", + " )\n", + " return shader\n", + "\n", + " def loadVertexShader(self, shaderName: str):\n", + " \"load vertex shader\"\n", + " return self.loadShader(shaderName, \"vertex\")\n", + "\n", + " def loadFragmentShader(self, shaderName: str):\n", + " \"load fragment shader\"\n", + " return self.loadShader(shaderName, \"fragment\")\n", + "\n", + " def getGlInfo(self):\n", + " \"Get opengl info\"\n", + " info = \"\"\"\n", + " Vendor: {0}\n", + " Renderer: {1}\n", + " OpenGL Version: {2}\n", + " Shader Version: {3}\n", + " \"\"\".format(\n", + " pygl.glGetString(pygl.GL_VENDOR),\n", + " pygl.glGetString(pygl.GL_RENDERER),\n", + " pygl.glGetString(pygl.GL_VERSION),\n", + " pygl.glGetString(pygl.GL_SHADING_LANGUAGE_VERSION)\n", + " )\n", + " return info\n", + "\n", + " def initializeGL(self):\n", + " print('gl initial')\n", + " print(self.getGlInfo())\n", + " # create context and make it current\n", + " self.context.create()\n", + " self.context.aboutToBeDestroyed.connect(self.cleanUpGl)\n", + " \n", + " # initialize functions\n", + " funcs = self.context.functions()\n", + " funcs.initializeOpenGLFunctions()\n", + " funcs.glClearColor(1, 1, 1, 1)\n", + "\n", + " # deal with shaders\n", + " shaderName = \"portableTriangle\"\n", + " vshader = self.loadVertexShader(shaderName)\n", + " fshader = self.loadFragmentShader(shaderName)\n", + "\n", + " # creating shader program\n", + " self.program = QOpenGLShaderProgram(self.context)\n", + " self.program.addShader(vshader) # adding vertex shader\n", + " self.program.addShader(fshader) # adding fragment shader\n", + "\n", + " # bind attribute to a location\n", + " self.program.bindAttributeLocation(\"aPos\", 0)\n", + "\n", + " # link shader program\n", + " isLinked = self.program.link()\n", + " print(\"shader program is linked: \", isLinked)\n", + "\n", + " # bind the program\n", + " self.program.bind()\n", + "\n", + " # specify uniform value\n", + " colorLoc = self.program.uniformLocation(\"color\")\n", + " self.program.setUniformValue(colorLoc,\n", + " self.triangleColor)\n", + "\n", + " # self.useShader(\"triangle\")\n", + "\n", + " # deal with vao and vbo\n", + "\n", + " # create vao and vbo\n", + "\n", + " # vao\n", + " isVao = self.vao.create()\n", + " vaoBinder = QOpenGLVertexArrayObject.Binder(self.vao)\n", + "\n", + " # vbo\n", + " isVbo = self.vbo.create()\n", + " isBound = self.vbo.bind()\n", + "\n", + " # check if vao and vbo are created\n", + " print('vao created: ', isVao)\n", + " print('vbo created: ', isVbo)\n", + "\n", + " floatSize = ctypes.sizeof(ctypes.c_float)\n", + "\n", + " # allocate space on buffer\n", + " self.vbo.allocate(self.vertexData.tobytes(),\n", + " floatSize * self.vertexData.size)\n", + " funcs.glEnableVertexAttribArray(0)\n", + " nullptr = VoidPtr(0)\n", + " funcs.glVertexAttribPointer(0,\n", + " 3,\n", + " int(pygl.GL_FLOAT),\n", + " int(pygl.GL_FALSE),\n", + " 3 * floatSize,\n", + " nullptr)\n", + " self.vbo.release()\n", + " self.program.release()\n", + " vaoBinder = None\n", + "\n", + " def cleanUpGl(self):\n", + " \"Clean up everything\"\n", + " self.context.makeCurrent()\n", + " self.vbo.destroy()\n", + " del self.program\n", + " self.program = None\n", + " self.doneCurrent()\n", + "\n", + " def resizeGL(self, width: int, height: int):\n", + " \"Resize the viewport\"\n", + " funcs = self.context.functions()\n", + " funcs.glViewport(0, 0, width, height)\n", + "\n", + " def paintGL(self):\n", + " \"drawing loop\"\n", + " funcs = self.context.functions()\n", + "\n", + " # clean up what was drawn\n", + " funcs.glClear(pygl.GL_COLOR_BUFFER_BIT)\n", + "\n", + " # actual drawing\n", + " vaoBinder = QOpenGLVertexArrayObject.Binder(self.vao)\n", + " self.program.bind()\n", + " funcs.glDrawArrays(pygl.GL_TRIANGLES,\n", + " 0,\n", + " 3)\n", + " self.program.release()\n", + " vaoBinder = None" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "All done! If you have come so far, congragulations. Try your triangle by executing the following cell." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "import os\n", + "import subprocess\n", + "subprocess.run(\n", + " [\"python\", os.path.join(\"myTriangle\", \"app.py\")]\n", + ")" ] } ], diff --git a/tutorials/01-triangle/gltriangle.py b/tutorials/01-triangle/gltriangle.py index 52d6506..2c773c2 100644 --- a/tutorials/01-triangle/gltriangle.py +++ b/tutorials/01-triangle/gltriangle.py @@ -131,9 +131,9 @@ def initializeGL(self): print(self.getGlInfo()) # create context and make it current self.context.create() - # surface = QSurface(QSurface.OpenGLSurface) - # self.context.makeCurrent(surface) self.context.aboutToBeDestroyed.connect(self.cleanUpGl) + + # initialize functions funcs = self.context.functions() funcs.initializeOpenGLFunctions() funcs.glClearColor(1, 1, 1, 1) @@ -225,4 +225,4 @@ def paintGL(self): 0, 3) self.program.release() - vaoBinder = None + vaoBinder = None \ No newline at end of file diff --git a/tutorials/02-rectangle/glrectangle.py b/tutorials/02-rectangle/glrectangle.py new file mode 100644 index 0000000..e69de29 diff --git a/tutorials/03-texture/gltexture.py b/tutorials/03-texture/gltexture.py new file mode 100644 index 0000000..0d029de --- /dev/null +++ b/tutorials/03-texture/gltexture.py @@ -0,0 +1,248 @@ +# author: Kaan Eraslan + +import numpy as np +import os +import sys +import ctypes + +from PySide2 import QtWidgets, QtCore, QtGui +from PySide2.QtGui import QVector3D +from PySide2.QtGui import QOpenGLVertexArrayObject +from PySide2.QtGui import QOpenGLBuffer +from PySide2.QtGui import QOpenGLShaderProgram +from PySide2.QtGui import QOpenGLShader +from PySide2.QtGui import QOpenGLContext +from PySide2.QtGui import QMatrix4x4 +from PySide2.QtGui import QVector4D +from PySide2.QtGui import QColor + +from PySide2.QtWidgets import QApplication +from PySide2.QtWidgets import QMessageBox +from PySide2.QtWidgets import QOpenGLWidget + +from PySide2.QtCore import QCoreApplication + +from PySide2.shiboken2 import VoidPtr + + +try: + from OpenGL import GL as pygl +except ImportError: + app = QApplication(sys.argv) + messageBox = QMessageBox(QMessageBox.Critical, "OpenGL hellogl", + "PyOpenGL must be installed to run this example.", + QMessageBox.Close) + messageBox.setDetailedText( + "Run:\npip install PyOpenGL PyOpenGL_accelerate") + messageBox.exec_() + sys.exit(1) + + +class TextureGL(QOpenGLWidget): + "Texture loading opengl widget" + + def __init__(self, parent=None): + "" + QOpenGLWidget.__init__(self, parent) + + # media and project structure + projectdir = os.getcwd() + projectdir = os.path.join(projectdir, "qtopengl") + assetsdir = os.path.join(projectdir, "assets") + self.imagesdir = os.path.join(assetsdir, 'images') + self.shadersdir = os.path.join(assetsdir, 'shaders') + self.availableShaders = [ + "basic_color", + "simpleLamp", + "triangle", + "portableTriangle", + "portableTexture" + ] + self.shaders = { + name: {"vertex": os.path.join(self.shadersdir, name + ".vert"), + "fragment": os.path.join(self.shadersdir, name + ".vert")} + for name in self.availableShaders + } + + self.core = "--coreprofile" in QCoreApplication.arguments() + + # opengl data related + self.context = QOpenGLContext() + self.vao = QOpenGLVertexArrayObject() + self.vbo = QOpenGLBuffer(QOpenGLBuffer.VertexBuffer) + self.ebo = QOpenGLBuffer(QOpenGLBuffer.IndexBuffer) + self.program = QOpenGLShaderProgram() + + # shader uniform attribute related + self.projectionMatrix = QMatrix4x4() + self.cameraMatrix = QMatrix4x4() + self.worldMatrix = QMatrix4x4() + + # locations of the attributes and uniforms + self.projectionMatrixLoc = 0 + self.cameraMatrixLoc = 0 + self.normalMatrixLoc = 0 + self.lightPositionLoc = 0 + + # some vertex data for corners of rectangle that would contain texture + + self.vertexData = np.array([ # viewport position xyz | colors xyz | texture coordinates xy + 0.5, 0.5, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, # top right + 0.5, -0.5, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, # bottom right + -0.5, -0.5, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, # bottom let + -0.5, 0.5, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0 # top let + ], + dtype=ctypes.c_float + ) + + def loadShader(self, + shaderName: str, + shaderType: str): + "Load shader" + shader = self.shaders[shaderName] + shaderpath = shader[shaderType] + if shaderType == "vertex": + shader = QOpenGLShader(QOpenGLShader.Vertex) + else: + shader = QOpenGLShader(QOpenGLShader.Fragment) + # + isCompiled = shader.compileSourceFile(shaderpath) + + if isCompiled is False: + print(shader.log()) + raise ValueError( + "{0} shader {1} in {2} is not compiled".format( + shaderType, shaderName, shaderpath + ) + ) + return shader + + def loadVertexShader(self, shaderName: str): + "load vertex shader" + return self.loadShader(shaderName, "vertex") + + def loadFragmentShader(self, shaderName: str): + "load fragment shader" + return self.loadShader(shaderName, "fragment") + + def getGlInfo(self): + "Get opengl info" + info = """ + Vendor: {0} + Renderer: {1} + OpenGL Version: {2} + Shader Version: {3} + """.format( + pygl.glGetString(pygl.GL_VENDOR), + pygl.glGetString(pygl.GL_RENDERER), + pygl.glGetString(pygl.GL_VERSION), + pygl.glGetString(pygl.GL_SHADING_LANGUAGE_VERSION) + ) + return info + + def initializeGL(self): + print('gl initial') + print(self.getGlInfo()) + # create context and make it current + self.context.create() + # surface = QSurface(QSurface.OpenGLSurface) + # self.context.makeCurrent(surface) + self.context.aboutToBeDestroyed.connect(self.cleanUpGl) + funcs = self.context.functions() + funcs.initializeOpenGLFunctions() + funcs.glClearColor(1, 1, 1, 1) + + # deal with shaders + shaderName = "portableTriangle" + vshader = self.loadVertexShader(shaderName) + fshader = self.loadFragmentShader(shaderName) + + # creating shader program + self.program = QOpenGLShaderProgram(self.context) + self.program.addShader(vshader) # adding vertex shader + self.program.addShader(fshader) # adding fragment shader + + # bind attribute to a location + attrLocations = {"aPos": 0, + "aColor": 1, + "aTexCoord": 2} + self.bindAttributes2ShaderProgram(attrLocations) + + # link shader program + isLinked = self.program.link() + print("shader program is linked: ", isLinked) + + # bind the program + self.program.bind() + + # rectangle indices + indices = np.array([0, 1, 3, 1, 2, 3], dtype=ctypes.c_float) + + # create vao, vbo and ebo + # vao + isVao = self.vao.create() + vaoBinder = QOpenGLVertexArrayObject.Binder(self.vao) + + # vbo + isVbo = self.vbo.create() + isVboBound = self.vbo.bind() + + # ebo + isEbo = self.ebo.create() + isEboBound = self.ebo.bind() + + # check if vao, vbo, ebo are created + print('vao created: ', isVao) + print('vbo created: ', isVbo) + print('ebo created: ', isEbo) + + # check if they are bound + print('vbo bound: ', isVboBound) + print('ebo bound: ', isEboBound) + + floatSize = ctypes.sizeof(ctypes.c_float) + nullptr = VoidPtr(0) + + # allocate space on vbo + self.vbo.allocate(self.vertexData.tobytes(), # data, + floatSize * self.vertexData.size) + + # allocate space on ebo + self.ebo.allocate(indices.tobytes(), indices.size * floatSize) + + # let's specify the attributes and point them + + # position attribute + funcs.glEnableVertexAttribArray(attrLocations['aPos']) + funcs.glVertexAttribPointer(attrLocations['aPos'], # location + 3, # size of attribute 3 for vec3 + int(pygl.GL_FLOAT), + int(pygl.GL_FALSE), + 8 * floatSize, + 0) + # color attribute + funcs.glEnableVertexAttribArray(attrLocations['aColor']) + funcs.glVertexAttribPointer(attrLocations['aColor'], # location + 3, # size of attribute 3 for vec3 + int(pygl.GL_FLOAT), + int(pygl.GL_FALSE), + 8 * floatSize, + 3 * floatSize) + # texture coordinate attribute + funcs.glEnableVertexAttribArray(attrLocations['aTexCoord']) + funcs.glVertexAttribPointer(attrLocations['aTexCoord'], # location + 2, # size of attribute 3 for vec2 + int(pygl.GL_FLOAT), + int(pygl.GL_FALSE), + 8 * floatSize, # offset + 6 * floatSize # stride + ) + + + + + + def bindAttributes2ShaderProgram(self, attrLocations: dict): + "Bind attributes to shader program" + for attrName, location in attrLocations.items(): + self.program.bindAttributeLocation(attrName, location)