├── README.md ├── ScreenShots └── 2d-triangle.png ├── _001_show_a_simple_window ├── SimpleRect.qml ├── __init__.py └── main.py ├── _002_2d_triangle ├── TriangleWindow.qml ├── __init__.py ├── main.py ├── shaders │ ├── OpenGL_2_1 │ │ ├── fragment.glsl │ │ └── vertex.glsl │ └── OpenGL_4_1 │ │ ├── fragment.glsl │ │ └── vertex.glsl └── triangle.py ├── _003_3d_rotating_triangle ├── TriangleWindow.qml ├── __init__.py ├── main.py ├── shaders │ ├── OpenGL_2_1 │ │ ├── fragment.glsl │ │ └── vertex.glsl │ └── OpenGL_4_1 │ │ ├── fragment.glsl │ │ └── vertex.glsl ├── triangle.py ├── unittests.py └── utils.py └── _004_3d_loading_model_and_rotating ├── __init__.py ├── geometries.py ├── main.py ├── mesh ├── bunny.obj └── cube.obj ├── qml ├── Button.qml └── ModelWindow.qml ├── render_engine.py ├── shaders ├── OpenGL_2_1 │ ├── fragment.glsl │ └── vertex.glsl └── OpenGL_4_1 │ ├── fragment.glsl │ └── vertex.glsl └── utils.py /README.md: -------------------------------------------------------------------------------- 1 | # QtQuick2 PyQt5 OpenGL Example 2 | 3 | Experimental examples of how to integrate PyQt5, QtQuick, and raw OpenGL calls together. 4 | 5 | * __001 : Just a blank window__: 6 | Nothing to show. 7 | 8 | * __002 : 2d triangle example__: 9 | Click on the buttons on the right side will change the color of the triangle. 10 | ![2d triangle example](https://github.com/kunlin596/qtquick2-python-opengl-example/blob/master/ScreenShots/2d-triangle.png) 11 | -------------------------------------------------------------------------------- /ScreenShots/2d-triangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kunlin596/OpenGL-QML-PyQt5-Examples/815887ccc4e31f1c5d41197204842566c4d93463/ScreenShots/2d-triangle.png -------------------------------------------------------------------------------- /_001_show_a_simple_window/SimpleRect.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | Rectangle { 4 | width: 640 5 | height: 480 6 | 7 | color: 'black' 8 | 9 | Text { 10 | text: '001 Simple Window Example' 11 | anchors { 12 | left: parent.left 13 | top: parent.top 14 | margins: 20 15 | } 16 | 17 | font.pointSize: 10 18 | color: 'white' 19 | } 20 | 21 | Text { 22 | anchors.centerIn: parent 23 | // anchors.fill: parent 24 | text: 'Hello World!' 25 | font.pointSize: 30 26 | color: 'white' 27 | } 28 | } -------------------------------------------------------------------------------- /_001_show_a_simple_window/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kunlin596/OpenGL-QML-PyQt5-Examples/815887ccc4e31f1c5d41197204842566c4d93463/_001_show_a_simple_window/__init__.py -------------------------------------------------------------------------------- /_001_show_a_simple_window/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from PyQt5.QtCore import QUrl 4 | from PyQt5.QtGui import QGuiApplication 5 | from PyQt5.QtQuick import QQuickView 6 | 7 | if __name__ == '__main__': 8 | app = QGuiApplication(sys.argv) 9 | 10 | view = QQuickView() 11 | view.setSource(QUrl('SimpleRect.qml')) 12 | view.setResizeMode(QQuickView.SizeRootObjectToView) # Set for the object to resize correctly 13 | 14 | view.show() 15 | 16 | app.exec() 17 | -------------------------------------------------------------------------------- /_002_2d_triangle/TriangleWindow.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import OpenGLUnderQml 1.0 3 | import QtGraphicalEffects 1.0 4 | 5 | Item { 6 | width: 640 7 | height: 320 8 | 9 | TriangleUnderlay { 10 | id: triangle 11 | } 12 | 13 | Text { 14 | text: '002 2D Triangle Example' 15 | anchors { 16 | left: parent.left 17 | top: parent.top 18 | margins: 20 19 | } 20 | 21 | font.pointSize: 10 22 | color: 'white' 23 | } 24 | 25 | Rectangle { 26 | id: button1 27 | 28 | width: 200 29 | height: 50 30 | 31 | color: Qt.rgba(0.9, 0.1, 0.1, 1.0) 32 | 33 | anchors.right: parent.right 34 | anchors.topMargin: 20 35 | 36 | Behavior on width { NumberAnimation { duration: 100 } } 37 | 38 | Text { 39 | text: 'Red' 40 | anchors.centerIn: parent 41 | font.pointSize: 20 42 | color: 'white' 43 | } 44 | 45 | MouseArea 46 | { 47 | anchors.fill: parent 48 | hoverEnabled: true 49 | 50 | onEntered: { 51 | button1.width = 210 52 | } 53 | 54 | onExited: { 55 | button1.width = 190 56 | } 57 | 58 | onClicked: { 59 | triangle.changeColor(1) 60 | } 61 | } 62 | } 63 | 64 | Rectangle { 65 | id: button2 66 | 67 | width: 200 68 | height: 50 69 | 70 | color: Qt.rgba(0.1, 1.0, 0.1, 1.0) 71 | 72 | anchors.top: button1.bottom 73 | anchors.right: parent.right 74 | anchors.topMargin: 20 75 | 76 | Behavior on width { NumberAnimation { duration: 100 } } 77 | 78 | Text { 79 | text: 'Green' 80 | anchors.centerIn: parent 81 | font.pointSize: 20 82 | color: 'white' 83 | } 84 | 85 | MouseArea 86 | { 87 | anchors.fill: parent 88 | hoverEnabled: true 89 | 90 | onEntered: { 91 | button2.width = 210 92 | } 93 | 94 | onExited: { 95 | button2.width = 190 96 | } 97 | 98 | onClicked: { 99 | triangle.changeColor(2) 100 | } 101 | } 102 | } 103 | 104 | Rectangle { 105 | id: button3 106 | 107 | width: 200 108 | height: 50 109 | 110 | 111 | color: Qt.rgba(0.1, 0.1, 1.0, 1.0) 112 | 113 | anchors.top: button2.bottom 114 | anchors.right: parent.right 115 | anchors.topMargin: 20 116 | 117 | Behavior on width { NumberAnimation { duration: 100 } } 118 | 119 | Text { 120 | text: 'Blue' 121 | anchors.centerIn: parent 122 | font.pointSize: 20 123 | color: 'white' 124 | } 125 | 126 | MouseArea 127 | { 128 | anchors.fill: parent 129 | hoverEnabled: true 130 | 131 | onEntered: { 132 | button3.width = 210 133 | } 134 | 135 | onExited: { 136 | button3.width = 190 137 | } 138 | 139 | onClicked: { 140 | triangle.changeColor(3) 141 | } 142 | 143 | } 144 | } 145 | 146 | Rectangle { 147 | id: button4 148 | 149 | width: 200 150 | height: 50 151 | 152 | anchors.top: button3.bottom 153 | anchors.right: parent.right 154 | anchors.topMargin: 20 155 | 156 | Behavior on width { NumberAnimation { duration: 100 } } 157 | 158 | LinearGradient { 159 | anchors.fill: parent 160 | start: Qt.point(0, 0) 161 | end: Qt.point(button4.width, 0) 162 | gradient: Gradient { 163 | GradientStop { position: 0.0; color: "red" } 164 | GradientStop { position: 0.5; color: "green" } 165 | GradientStop { position: 1.0; color: "blue" } 166 | } 167 | } 168 | 169 | Text { 170 | text: '3 Colors' 171 | anchors.centerIn: parent 172 | font.pointSize: 20 173 | color: 'white' 174 | } 175 | 176 | MouseArea 177 | { 178 | anchors.fill: parent 179 | hoverEnabled: true 180 | 181 | onEntered: { 182 | button4.width = 210 183 | } 184 | 185 | onExited: { 186 | button4.width = 190 187 | } 188 | 189 | onClicked: { 190 | triangle.changeColor(4) 191 | } 192 | } 193 | } 194 | 195 | } -------------------------------------------------------------------------------- /_002_2d_triangle/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kunlin596/OpenGL-QML-PyQt5-Examples/815887ccc4e31f1c5d41197204842566c4d93463/_002_2d_triangle/__init__.py -------------------------------------------------------------------------------- /_002_2d_triangle/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from PyQt5.QtCore import QUrl 4 | from PyQt5.QtGui import QGuiApplication, QSurfaceFormat 5 | from PyQt5.QtQuick import QQuickView 6 | from PyQt5.QtQml import qmlRegisterType 7 | 8 | import platform 9 | 10 | from _002_2d_triangle.triangle import TriangleUnderlay 11 | 12 | if __name__ == '__main__': 13 | # Not working in Ubuntu 16.04, result in 1282 error for simple calling like glViewport(...) 14 | if platform.uname().system == 'Darwin': 15 | f = QSurfaceFormat() 16 | f.setVersion(4, 1) 17 | f.setDepthBufferSize(1) # fix depth buffer error 18 | f.setStencilBufferSize(1) # fix stencil buffer error 19 | 20 | # If CoreProfile is used, all the other QML rendering will fail, because they only use 2.1 21 | f.setProfile(QSurfaceFormat.CompatibilityProfile) 22 | QSurfaceFormat.setDefaultFormat(f) 23 | 24 | qmlRegisterType(TriangleUnderlay, 'OpenGLUnderQml', 1, 0, 'TriangleUnderlay') 25 | 26 | app = QGuiApplication(sys.argv) 27 | 28 | view = QQuickView() 29 | view.setFormat(f) 30 | view.setPersistentSceneGraph(True) 31 | view.setPersistentOpenGLContext(True) 32 | view.setResizeMode(QQuickView.SizeRootObjectToView) # Set for the object to resize correctly 33 | view.setSource(QUrl('TriangleWindow.qml')) 34 | view.show() 35 | 36 | app.exec() 37 | -------------------------------------------------------------------------------- /_002_2d_triangle/shaders/OpenGL_2_1/fragment.glsl: -------------------------------------------------------------------------------- 1 | varying highp vec3 pass_color; 2 | 3 | void main () { 4 | gl_FragColor = vec4(pass_color, 1.0); 5 | } 6 | -------------------------------------------------------------------------------- /_002_2d_triangle/shaders/OpenGL_2_1/vertex.glsl: -------------------------------------------------------------------------------- 1 | attribute highp vec3 position; 2 | attribute highp vec3 color; 3 | 4 | varying vec3 pass_color; 5 | 6 | void main () { 7 | gl_Position = vec4(position, 1.0); 8 | pass_color = color; 9 | } 10 | -------------------------------------------------------------------------------- /_002_2d_triangle/shaders/OpenGL_4_1/fragment.glsl: -------------------------------------------------------------------------------- 1 | #version 410 2 | 3 | in vec3 pass_color; 4 | 5 | out vec4 out_color; 6 | 7 | void main () { 8 | out_color = vec4(pass_color, 1.0); 9 | } 10 | -------------------------------------------------------------------------------- /_002_2d_triangle/shaders/OpenGL_4_1/vertex.glsl: -------------------------------------------------------------------------------- 1 | #version 410 2 | 3 | layout (location = 0) in vec3 position; 4 | layout (location = 1) in vec3 color; 5 | 6 | uniform mat4 model_matrix; 7 | uniform mat4 view_matrix; 8 | uniform mat4 projection_matrix; 9 | 10 | out vec3 pass_color; 11 | 12 | void main () { 13 | mat4 m = projection_matrix * view_matrix * model_matrix; 14 | gl_Position = m * vec4(position, 1.0); 15 | pass_color = color; 16 | } 17 | -------------------------------------------------------------------------------- /_002_2d_triangle/triangle.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QSize 2 | from PyQt5.QtQuick import QQuickItem 3 | from PyQt5.QtCore import Qt 4 | 5 | from PyQt5.QtGui import QOpenGLShaderProgram, QOpenGLShader 6 | 7 | positions = [ 8 | (-0.5, -0.8, 0.0), 9 | (0.5, -0.8, 0.0), 10 | (0.0, 0.8, 0.0) 11 | ] 12 | 13 | colors_mixed = [ 14 | (1.0, 0.0, 0.0), 15 | (0.0, 1.0, 0.0), 16 | (0.0, 0.0, 1.0) 17 | ] 18 | 19 | colors_red = [ 20 | (1.0, 0.0, 0.0, 0.0), 21 | (1.0, 0.1, 0.0, 0.0), 22 | (1.0, 0.1, 0.1, 0.0) 23 | ] 24 | 25 | colors_green = [ 26 | (0.0, 1.0, 0.0, 0.0), 27 | (0.1, 1.0, 0.0, 0.0), 28 | (0.1, 1.0, 0.1, 0.0) 29 | ] 30 | colors_blue = [ 31 | (0.0, 0.0, 1.0, 0.0), 32 | (0.0, 0.1, 1.0, 0.0), 33 | (0.1, 0.1, 1.0, 0.0) 34 | ] 35 | 36 | colors = colors_mixed 37 | 38 | class TriangleUnderlay(QQuickItem): 39 | def __init__ ( self, parent = None ): 40 | super(TriangleUnderlay, self).__init__(parent) 41 | self._renderer = None 42 | self.windowChanged.connect(self.onWindowChanged) 43 | 44 | # @pyqtSlot('QQuickWindow'), incompatible connection error, don't know why 45 | def onWindowChanged ( self, window ): 46 | # Because it's in different thread which required a direct connection 47 | # window == self.window(), they are pointing to the same window instance. Verified. 48 | window.beforeSynchronizing.connect(self.sync, type = Qt.DirectConnection) 49 | window.setClearBeforeRendering(False) # otherwise quick would clear everything we render 50 | 51 | @pyqtSlot(name = 'sync') 52 | def sync ( self ): 53 | if self._renderer is None: 54 | # QObject: Cannot create children for a parent that is in a different thread. 55 | # (Parent is TriangleUnderlay(0x7fd0d64734e0), parent's thread is QThread(0x7fd0d6197270), current thread is QSGRenderThread(0x7fd0d70b9210) 56 | # TriangleUnderlay should NOT be TriangleUnderlayRenderer's parent 57 | self._renderer = TriangleUnderlayRenderer() 58 | # Because it's in different thread which required a direct connection 59 | self.window().beforeRendering.connect(self._renderer.paint, type = Qt.DirectConnection) 60 | self._renderer.set_viewport_size(self.window().size() * self.window().devicePixelRatio()) 61 | self._renderer.set_window(self.window()) 62 | 63 | @pyqtSlot(int) 64 | def changeColor ( self, color_enum ): 65 | global colors 66 | if color_enum == 1: 67 | colors = colors_red 68 | elif color_enum == 2: 69 | colors = colors_green 70 | elif color_enum == 3: 71 | colors = colors_blue 72 | elif color_enum == 4: 73 | colors = colors_mixed 74 | 75 | 76 | class TriangleUnderlayRenderer(QObject): 77 | def __init__ ( self, parent = None ): 78 | super(TriangleUnderlayRenderer, self).__init__(parent) 79 | self._shader_program = None 80 | self._viewport_size = QSize() 81 | self._window = None 82 | 83 | @pyqtSlot() 84 | def paint ( self ): 85 | 86 | # TODO test on Ubuntu 87 | # for Darwin, it's a must 88 | gl = self._window.openglContext().versionFunctions() 89 | 90 | if self._shader_program is None: 91 | self._shader_program = QOpenGLShaderProgram() 92 | self._shader_program.addShaderFromSourceFile(QOpenGLShader.Vertex, 'shaders/OpenGL_2_1/vertex.glsl') 93 | self._shader_program.addShaderFromSourceFile(QOpenGLShader.Fragment, 'shaders/OpenGL_2_1/fragment.glsl') 94 | self._shader_program.bindAttributeLocation('position', 0) 95 | self._shader_program.bindAttributeLocation('color', 1) 96 | self._shader_program.link() 97 | 98 | self._shader_program.bind() 99 | self._shader_program.enableAttributeArray(0) 100 | self._shader_program.enableAttributeArray(1) 101 | 102 | self._shader_program.setAttributeArray(0, positions) 103 | self._shader_program.setAttributeArray(1, colors) 104 | 105 | gl.glViewport(0, 0, self._viewport_size.width(), self._viewport_size.height()) 106 | 107 | gl.glClearColor(0.5, 0.5, 0.5, 1) 108 | gl.glDisable(gl.GL_DEPTH_TEST) 109 | 110 | gl.glClear(gl.GL_COLOR_BUFFER_BIT) 111 | 112 | gl.glDrawArrays(gl.GL_TRIANGLES, 0, 3) 113 | 114 | self._shader_program.disableAttributeArray(0) 115 | self._shader_program.disableAttributeArray(1) 116 | 117 | self._shader_program.release() 118 | 119 | # Restore the OpenGL state for QtQuick rendering 120 | self._window.resetOpenGLState() 121 | self._window.update() 122 | 123 | def set_viewport_size ( self, size ): 124 | self._viewport_size = size 125 | 126 | def set_window ( self, window ): 127 | self._window = window 128 | -------------------------------------------------------------------------------- /_003_3d_rotating_triangle/TriangleWindow.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import OpenGLUnderQml 1.0 3 | import QtGraphicalEffects 1.0 4 | 5 | Item { 6 | width: 640 7 | height: 320 8 | 9 | TriangleUnderlay { 10 | id: triangle 11 | theta: 0.0 12 | } 13 | 14 | Timer { 15 | interval: 20 16 | running: true 17 | repeat: true 18 | onTriggered: { 19 | triangle.theta = triangle.theta + 2.0 20 | if (triangle.theta == 360.0) { 21 | triangle.theta = 0.0 22 | } 23 | } 24 | } 25 | 26 | Text { 27 | text: '003 3D Rotating Triangle Example' 28 | anchors { 29 | left: parent.left 30 | top: parent.top 31 | margins: 20 32 | } 33 | 34 | font.pointSize: 10 35 | color: 'white' 36 | } 37 | 38 | Rectangle { 39 | id: button1 40 | 41 | width: 200 42 | height: 50 43 | 44 | color: Qt.rgba(0.9, 0.1, 0.1, 1.0) 45 | 46 | anchors.right: parent.right 47 | 48 | 49 | Behavior on width { NumberAnimation { duration: 50 } } 50 | 51 | Text { 52 | text: 'Red' 53 | anchors.centerIn: parent 54 | font.pointSize: 20 55 | color: 'white' 56 | } 57 | 58 | MouseArea 59 | { 60 | anchors.fill: parent 61 | hoverEnabled: true 62 | 63 | onEntered: { 64 | button1.width = 210 65 | } 66 | 67 | onExited: { 68 | button1.width = 190 69 | } 70 | 71 | onClicked: { 72 | triangle.changeColor(1) 73 | } 74 | } 75 | } 76 | 77 | Rectangle { 78 | id: button2 79 | 80 | width: 200 81 | height: 50 82 | 83 | color: Qt.rgba(0.1, 1.0, 0.1, 1.0) 84 | 85 | anchors.top: button1.bottom 86 | anchors.right: parent.right 87 | 88 | 89 | Behavior on width { NumberAnimation { duration: 50 } } 90 | 91 | Text { 92 | text: 'Green' 93 | anchors.centerIn: parent 94 | font.pointSize: 20 95 | color: 'white' 96 | } 97 | 98 | MouseArea 99 | { 100 | anchors.fill: parent 101 | hoverEnabled: true 102 | 103 | onEntered: { 104 | button2.width = 210 105 | } 106 | 107 | onExited: { 108 | button2.width = 190 109 | } 110 | 111 | onClicked: { 112 | triangle.changeColor(2) 113 | } 114 | } 115 | } 116 | 117 | Rectangle { 118 | id: button3 119 | 120 | width: 200 121 | height: 50 122 | 123 | color: Qt.rgba(0.1, 0.1, 1.0, 1.0) 124 | 125 | anchors.top: button2.bottom 126 | anchors.right: parent.right 127 | 128 | Behavior on width { NumberAnimation { duration: 50 } } 129 | 130 | Text { 131 | text: 'Blue' 132 | anchors.centerIn: parent 133 | font.pointSize: 20 134 | color: 'white' 135 | } 136 | 137 | MouseArea 138 | { 139 | anchors.fill: parent 140 | hoverEnabled: true 141 | 142 | onEntered: { 143 | button3.width = 210 144 | } 145 | 146 | onExited: { 147 | button3.width = 190 148 | } 149 | 150 | onClicked: { 151 | triangle.changeColor(3) 152 | } 153 | } 154 | } 155 | 156 | Rectangle { 157 | id: button4 158 | 159 | width: 200 160 | height: 50 161 | 162 | anchors.top: button3.bottom 163 | anchors.right: parent.right 164 | 165 | Behavior on width { NumberAnimation { duration: 50 } } 166 | 167 | LinearGradient { 168 | anchors.fill: parent 169 | start: Qt.point(0, 0) 170 | end: Qt.point(button4.width, 0) 171 | gradient: Gradient { 172 | GradientStop { position: 0.0; color: "red" } 173 | GradientStop { position: 0.5; color: "green" } 174 | GradientStop { position: 1.0; color: "blue" } 175 | } 176 | } 177 | 178 | Text { 179 | text: '3 Colors' 180 | anchors.centerIn: parent 181 | font.pointSize: 20 182 | color: 'white' 183 | } 184 | 185 | MouseArea 186 | { 187 | anchors.fill: parent 188 | hoverEnabled: true 189 | 190 | onEntered: { 191 | button4.width = 210 192 | } 193 | 194 | onExited: { 195 | button4.width = 190 196 | } 197 | 198 | onClicked: { 199 | triangle.changeColor(4) 200 | } 201 | } 202 | } 203 | } -------------------------------------------------------------------------------- /_003_3d_rotating_triangle/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kunlin596/OpenGL-QML-PyQt5-Examples/815887ccc4e31f1c5d41197204842566c4d93463/_003_3d_rotating_triangle/__init__.py -------------------------------------------------------------------------------- /_003_3d_rotating_triangle/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from PyQt5.QtCore import QUrl 4 | from PyQt5.QtGui import QGuiApplication, QOpenGLVersionProfile, QSurfaceFormat 5 | from PyQt5.QtQuick import QQuickView 6 | from PyQt5.QtQml import qmlRegisterType 7 | 8 | from _003_3d_rotating_triangle.triangle import TriangleUnderlay 9 | import platform 10 | 11 | if __name__ == '__main__': 12 | # Not working in Ubuntu 16.04, result in 1282 error for simple calling like glViewport(...) 13 | # TODO 14 | 15 | if platform.uname().system == 'Darwin': 16 | f = QSurfaceFormat() 17 | f.setVersion(4, 1) 18 | f.setDepthBufferSize(1) # fix depth buffer error 19 | f.setStencilBufferSize(1) # fix stencil buffer error 20 | 21 | # If CoreProfile is used, all the other QML rendering will fail, because they only use 2.1 22 | f.setProfile(QSurfaceFormat.CompatibilityProfile) 23 | QSurfaceFormat.setDefaultFormat(f) 24 | 25 | qmlRegisterType(TriangleUnderlay, 'OpenGLUnderQml', 1, 0, 'TriangleUnderlay') 26 | 27 | app = QGuiApplication(sys.argv) 28 | 29 | view = QQuickView() 30 | # view.setFormat(f) 31 | view.setPersistentSceneGraph(True) 32 | view.setPersistentOpenGLContext(True) 33 | view.setResizeMode(QQuickView.SizeRootObjectToView) # Set for the object to resize correctly 34 | view.setSource(QUrl('TriangleWindow.qml')) 35 | view.show() 36 | 37 | app.exec() 38 | -------------------------------------------------------------------------------- /_003_3d_rotating_triangle/shaders/OpenGL_2_1/fragment.glsl: -------------------------------------------------------------------------------- 1 | varying highp vec3 pass_color; 2 | 3 | void main () { 4 | gl_FragColor = vec4(pass_color, 1.0); 5 | } 6 | -------------------------------------------------------------------------------- /_003_3d_rotating_triangle/shaders/OpenGL_2_1/vertex.glsl: -------------------------------------------------------------------------------- 1 | attribute highp vec3 position; 2 | attribute highp vec3 color; 3 | uniform highp mat4 model_matrix; 4 | uniform highp mat4 view_matrix; 5 | uniform highp mat4 projection_matrix; 6 | 7 | varying vec3 pass_color; 8 | 9 | void main () { 10 | mat4 m = projection_matrix * view_matrix * model_matrix; 11 | gl_Position = m * vec4(position, 1.0); 12 | pass_color = color; 13 | } 14 | -------------------------------------------------------------------------------- /_003_3d_rotating_triangle/shaders/OpenGL_4_1/fragment.glsl: -------------------------------------------------------------------------------- 1 | #version 410 2 | 3 | in vec3 pass_color; 4 | 5 | out vec4 out_color; 6 | 7 | void main () { 8 | out_color = vec4(pass_color, 1.0); 9 | } 10 | -------------------------------------------------------------------------------- /_003_3d_rotating_triangle/shaders/OpenGL_4_1/vertex.glsl: -------------------------------------------------------------------------------- 1 | #version 410 2 | 3 | layout (location = 0) in vec3 position; 4 | layout (location = 1) in vec3 color; 5 | 6 | uniform mat4 model_matrix; 7 | uniform mat4 view_matrix; 8 | uniform mat4 projection_matrix; 9 | 10 | out vec3 pass_color; 11 | 12 | void main () { 13 | mat4 m = projection_matrix * view_matrix * model_matrix; 14 | gl_Position = m * vec4(position, 1.0); 15 | pass_color = color; 16 | } 17 | -------------------------------------------------------------------------------- /_003_3d_rotating_triangle/triangle.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QSize 2 | from PyQt5.QtQuick import QQuickItem 3 | from PyQt5.QtCore import Qt 4 | 5 | from PyQt5.QtGui import QOpenGLShaderProgram, QOpenGLShader, QMatrix4x4, QOpenGLContext 6 | from _003_3d_rotating_triangle.utils import * 7 | from _003_3d_rotating_triangle.utils import Camera 8 | 9 | import platform as pf 10 | import sys 11 | 12 | if pf.uname().system == 'Linux': 13 | try: 14 | import OpenGL.GL as GL 15 | except ImportError as e: 16 | GL = None 17 | print('can\'t import OpenGL') 18 | sys.exit(1) 19 | 20 | positions = [ 21 | (-5.0, -5.0, 0.0), 22 | (5.0, -5.0, 0.0), 23 | (0.0, 5.0, 0.0) 24 | ] 25 | 26 | colors_mixed = [ 27 | (1.0, 0.0, 0.0), 28 | (0.0, 1.0, 0.0), 29 | (0.0, 0.0, 1.0) 30 | ] 31 | 32 | colors_red = [ 33 | (1.0, 0.0, 0.0, 0.0), 34 | (1.0, 0.1, 0.0, 0.0), 35 | (1.0, 0.1, 0.1, 0.0) 36 | ] 37 | 38 | colors_green = [ 39 | (0.0, 1.0, 0.0, 0.0), 40 | (0.1, 1.0, 0.0, 0.0), 41 | (0.1, 1.0, 0.1, 0.0) 42 | ] 43 | colors_blue = [ 44 | (0.0, 0.0, 1.0, 0.0), 45 | (0.0, 0.1, 1.0, 0.0), 46 | (0.1, 0.1, 1.0, 0.0) 47 | ] 48 | 49 | colors = colors_mixed 50 | 51 | 52 | class TriangleUnderlay(QQuickItem): 53 | theta_changed = pyqtSignal(name = 'theta_changed') # the optional unbound notify signal. Probably no need herel 54 | 55 | def __init__ ( self, parent = None ): 56 | super(TriangleUnderlay, self).__init__(parent) 57 | self._renderer = None 58 | self.windowChanged.connect(self.onWindowChanged) 59 | 60 | self._theta = 0.0 61 | 62 | # @pyqtSlot('QQuickWindow'), incompatible connection error, don't know why 63 | def onWindowChanged ( self, window ): 64 | # Because it's in different thread which required a direct connection 65 | # window == self.window(), they are pointing to the same window instance. Verified. 66 | window.beforeSynchronizing.connect(self.sync, type = Qt.DirectConnection) 67 | window.setClearBeforeRendering(False) # otherwise quick would clear everything we render 68 | 69 | @pyqtSlot(name = 'sync') 70 | def sync ( self ): 71 | if self._renderer is None: 72 | self._renderer = TriangleUnderlayRenderer() 73 | self.window().beforeRendering.connect(self._renderer.paint, type = Qt.DirectConnection) 74 | self._renderer.set_viewport_size(self.window().size() * self.window().devicePixelRatio()) 75 | self._renderer.set_window(self.window()) 76 | self._renderer.set_theta(self._theta) 77 | self._renderer.set_projection_matrix() 78 | 79 | @pyqtSlot(int) 80 | def changeColor ( self, color_enum ): 81 | global colors 82 | if color_enum == 1: 83 | colors = colors_red 84 | elif color_enum == 2: 85 | colors = colors_green 86 | elif color_enum == 3: 87 | colors = colors_blue 88 | elif color_enum == 4: 89 | colors = colors_mixed 90 | 91 | @pyqtProperty('float', notify = theta_changed) 92 | def theta ( self ): 93 | return self._theta 94 | 95 | @theta.setter 96 | def theta ( self, theta ): 97 | if theta == self._theta: 98 | return 99 | self._theta = theta 100 | self.theta_changed.emit() 101 | 102 | if self.window(): 103 | self.window().update() 104 | 105 | 106 | class TriangleUnderlayRenderer(QObject): 107 | def __init__ ( self, parent = None ): 108 | super(TriangleUnderlayRenderer, self).__init__(parent) 109 | self._shader_program = None 110 | self._viewport_size = QSize() 111 | self._window = None 112 | self._camera = Camera() 113 | 114 | self._perspective_projection_matrix = perspective_projection(45.0, 4.0 / 3.0, 115 | 0.001, 100.0) 116 | 117 | self._orthographic_projection_matrix = orthographic_projection(640.0, 480.0, 118 | 0.001, 100.0) 119 | 120 | self._model_matrix = np.identity(4) 121 | 122 | self._projection_type = 0 123 | self._projection_matrix = self._perspective_projection_matrix 124 | 125 | self._theta = 0.0 126 | 127 | def set_theta ( self, theta ): 128 | self._theta = theta 129 | 130 | # around y axis 131 | def build_rotation_matrix ( self ): 132 | m = np.identity(4) 133 | m[0][0] = np.cos(np.radians(self._theta)) 134 | m[0][2] = np.sin(np.radians(self._theta)) 135 | m[2][0] = -np.sin(np.radians(self._theta)) 136 | m[2][2] = np.cos(np.radians(self._theta)) 137 | return m 138 | 139 | @pyqtSlot(int) 140 | def setProjectionType ( self, t ): 141 | if t != self._projection_type: 142 | self._projection_type = t 143 | 144 | @pyqtSlot() 145 | def paint ( self ): 146 | # for Darwin, it's a must 147 | if pf.uname().system == 'Darwin': 148 | global GL 149 | GL = self._window.openglContext().versionFunctions() 150 | 151 | w = self._viewport_size.width() 152 | h = self._viewport_size.height() 153 | 154 | GL.glViewport(0, 0, int(w), int(h)) 155 | 156 | if self._shader_program is None: 157 | self._shader_program = QOpenGLShaderProgram() 158 | self._shader_program.addShaderFromSourceFile(QOpenGLShader.Vertex, 'shaders/OpenGL_2_1/vertex.glsl') 159 | self._shader_program.addShaderFromSourceFile(QOpenGLShader.Fragment, 'shaders/OpenGL_2_1/fragment.glsl') 160 | self._shader_program.bindAttributeLocation('position', 0) 161 | self._shader_program.bindAttributeLocation('color', 1) 162 | self._shader_program.link() 163 | 164 | self._shader_program.bind() 165 | self._shader_program.enableAttributeArray(0) 166 | self._shader_program.enableAttributeArray(1) 167 | 168 | self._shader_program.setAttributeArray(0, positions) 169 | self._shader_program.setAttributeArray(1, colors) 170 | 171 | if self._projection_type == 0: 172 | self._projection_matrix = self._perspective_projection_matrix 173 | elif self._projection_type == 1: 174 | self._projection_matrix = self._orthographic_projection_matrix 175 | 176 | self._model_matrix = self.build_rotation_matrix() 177 | 178 | self._shader_program.setUniformValue('model_matrix', 179 | QMatrix4x4(self._model_matrix.flatten().tolist())) 180 | 181 | self._shader_program.setUniformValue('view_matrix', 182 | QMatrix4x4(self._camera.get_view_matrix().flatten().tolist())) 183 | 184 | self._shader_program.setUniformValue('projection_matrix', 185 | QMatrix4x4(self._projection_matrix.flatten().tolist())) 186 | 187 | GL.glClearColor(0.2, 0.2, 0.2, 1) 188 | GL.glEnable(GL.GL_DEPTH_TEST) 189 | GL.glClear(GL.GL_COLOR_BUFFER_BIT) 190 | GL.glDrawArrays(GL.GL_TRIANGLES, 0, 3) 191 | 192 | self._shader_program.disableAttributeArray(0) 193 | self._shader_program.disableAttributeArray(1) 194 | 195 | self._shader_program.release() 196 | 197 | # Restore the OpenGL state for QtQuick rendering 198 | self._window.resetOpenGLState() 199 | self._window.update() 200 | 201 | def set_viewport_size ( self, size ): 202 | self._viewport_size = size 203 | 204 | def set_window ( self, window ): 205 | self._window = window 206 | 207 | def set_projection_matrix ( self ): 208 | # Need to be set every time we change the size of the window 209 | self._perspective_projection_matrix = perspective_projection(45.0, 210 | self._viewport_size.width() / self._viewport_size.height(), 211 | 0.001, 100.0) 212 | 213 | self._orthographic_projection_matrix = orthographic_projection(self._viewport_size.width(), 214 | self._viewport_size.height(), 215 | 0.001, 100.0) 216 | -------------------------------------------------------------------------------- /_003_3d_rotating_triangle/unittests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | import numpy.linalg as la 4 | from _003_3d_rotating_triangle.utils import Camera 5 | 6 | 7 | class TestCamera(unittest.TestCase): 8 | def test_camera_lookat ( self ): 9 | c = Camera() 10 | c.update_view_matrix() 11 | 12 | 13 | if __name__ == '__main__': 14 | unittest.main() 15 | -------------------------------------------------------------------------------- /_003_3d_rotating_triangle/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import numpy.linalg as la 3 | from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty 4 | from PyQt5.QtQml import QQmlListProperty 5 | import math 6 | 7 | 8 | def normalize_vector ( v ): 9 | return v / la.norm(v) 10 | 11 | 12 | class Camera(QObject): 13 | eye_changed = pyqtSignal(name = 'eye_changed') 14 | up_changed = pyqtSignal(name = 'up_changed') 15 | target_changed = pyqtSignal(name = 'target_changed') 16 | 17 | def __init__ ( self, parent = None ): 18 | super(Camera, self).__init__(parent) 19 | self._eye = np.array([0.0, 0.0, 30.0]) 20 | self._up = np.array([0.0, 1.0, 0.0]) 21 | self._target = np.array([0.0, 0.0, -20.0]) 22 | self.x = None 23 | self.y = None 24 | self.z = None 25 | 26 | self.mouse_x = 0.0 27 | self.mouse_y = 0.0 28 | self._m = np.identity(4) 29 | self.update_view_matrix() 30 | 31 | def get_view_matrix ( self ): 32 | # return self._m 33 | # TODO camera matrix is still buggy, to fix at next example 34 | m = np.identity(4) 35 | m[2][3] = -20.0 36 | return m 37 | 38 | def update_view_matrix ( self ): 39 | self.z = normalize_vector(self._target - self._eye) # positive z is pointing at screen 40 | self.x = normalize_vector(np.cross(self._up, self.z)) 41 | self.y = np.cross(self.z, self.x) 42 | 43 | self._m[0, :] = np.array([self.x[0], self.x[1], self.x[2], 0.0]) 44 | self._m[1, :] = np.array([self.y[0], self.y[1], self.y[2], 0.0]) 45 | self._m[2, :] = np.array([self.z[0], self.z[1], self.z[2], 0.0]) 46 | self._m[3, :] = np.array([self._eye[0], self._eye[1], self._eye[2], 1.0]) 47 | 48 | # TODO Will be implemented in the next version 49 | # 50 | # @pyqtSlot('float', name = 'move_vertically') 51 | # def move_vertically ( self, y ): 52 | # self._eye += y * normalize_vector(self.y) 53 | # self.update_view_matrix() 54 | # 55 | # @pyqtSlot('float', name = 'move_horizontally') 56 | # def move_horizontally ( self, x ): 57 | # self._eye += x * normalize_vector(self.x) 58 | # self.update_view_matrix() 59 | # 60 | # @pyqtSlot('float', name = 'move_horizontally') 61 | # def move_forward ( self, z ): 62 | # self._eye += z * normalize_vector(self.z) 63 | # self.update_view_matrix() 64 | # 65 | # @pyqtSlot() 66 | # def rotate_horizontally ( self ): 67 | # pass 68 | # 69 | # @pyqtSlot() 70 | # def rotate_vertically ( self ): 71 | # pass 72 | # 73 | # @pyqtProperty(QQmlListProperty, notify = eye_changed) 74 | # def eye ( self ): 75 | # return self._eye 76 | # 77 | # @eye.setter 78 | # def eye ( self, eye ): 79 | # self._eye = eye 80 | # 81 | # @pyqtProperty(QQmlListProperty, notify = up_changed) 82 | # def up ( self ): 83 | # return self._up 84 | # 85 | # @up.setter 86 | # def up ( self, up ): 87 | # self._up = up 88 | # 89 | # @pyqtProperty(QQmlListProperty, notify = target_changed) 90 | # def target ( self ): 91 | # return self._target 92 | # 93 | # @target.setter 94 | # def target ( self, target ): 95 | # self._target = target 96 | 97 | 98 | # Tested against glm::perspective 99 | def perspective_projection ( fovy, aspect_ratio, near_z, far_z ): 100 | m = np.zeros(shape = (4, 4)) 101 | 102 | t = near_z * math.tan(fovy / 2.0) # half width 103 | r = t * aspect_ratio # half height 104 | 105 | m[0][0] = near_z / r 106 | m[1][1] = near_z / t 107 | m[2][2] = -(far_z + near_z) / (far_z - near_z) 108 | m[2][3] = -2 * far_z * near_z / (far_z - near_z) 109 | m[3][2] = -1 110 | 111 | return m 112 | 113 | 114 | # assuming the volume is symmetric such that no need to specify l, r, t, b 115 | # Tested against glm::ortho(l, r, b, t, near_z, far_z) with 116 | # glm::mat4 p = glm::ortho(-320.0f, 320.0f, -240.0f, 240.0f, 0.001f, 100.0f); 117 | # TODO should be farther tested 118 | def orthographic_projection ( w, h, near_z, far_z ): 119 | m = np.zeros(shape = (4, 4)) 120 | 121 | m[0][0] = 2.0 / w 122 | m[1][1] = 2.0 / h 123 | m[2][2] = -2.0 / (far_z - near_z) 124 | m[2][3] = -(far_z + near_z) / (far_z - near_z) 125 | m[3][3] = 1 126 | 127 | return m 128 | -------------------------------------------------------------------------------- /_004_3d_loading_model_and_rotating/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kunlin596/OpenGL-QML-PyQt5-Examples/815887ccc4e31f1c5d41197204842566c4d93463/_004_3d_loading_model_and_rotating/__init__.py -------------------------------------------------------------------------------- /_004_3d_loading_model_and_rotating/geometries.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pyassimp as ai 3 | import random 4 | 5 | 6 | class BaseGeometry(object): 7 | def __init__ (self, color = None): 8 | self.id = -1 9 | self.vertices = None 10 | self.colors = None 11 | self.indices = None 12 | self.rotation = np.identity(3) 13 | self.translation = np.array([0.0, 0.0, 0.0]) 14 | 15 | self._scene = None 16 | self._mesh = None 17 | self.color = color 18 | 19 | def read (self, path): 20 | try: 21 | self._scene = ai.load(path) 22 | self._mesh = self._scene.meshes[0] 23 | self.vertices = self._mesh.vertices 24 | self.indices = self._mesh.faces.flatten() 25 | self.colors = self._mesh.colors 26 | 27 | if self.color is not None: 28 | self.colors = np.array([[self.color, self.color, self.color] for x in range(len(self.vertices))]) 29 | else: 30 | r = random.uniform(0.3, 1.0) 31 | g = random.uniform(0.3, 1.0) 32 | b = random.uniform(0.3, 1.0) 33 | self.colors = np.array([[r, g, b] for x in range(len(self.vertices))]) 34 | 35 | 36 | except Exception as e: 37 | print('Geometry reading error', e) 38 | 39 | def __del__ (self): 40 | ai.release(self._scene) 41 | 42 | def rotate (self, axis, angle): 43 | pass 44 | 45 | def translate (self, vec): 46 | pass 47 | 48 | def update (self): 49 | """ 50 | Update the data (position, color) 51 | :return: 52 | """ 53 | pass 54 | 55 | def change_color (self, color): 56 | if (0.0 < color) and (color < 1.0): 57 | self.color = [color for i in range(len(self.vertices))] 58 | 59 | 60 | class Cube(BaseGeometry): 61 | file_path = 'cube.obj' 62 | 63 | def __init__ (self, color = None): 64 | super(Cube, self).__init__(color) 65 | self.length = 1.0 66 | self.width = 1.0 67 | self.height = 1.0 68 | self.read(Cube.file_path) 69 | 70 | def update (self): 71 | for i in range(0, len(self.vertices), 3): 72 | self.vertices[i] = self.length if self.vertices[i] > 0 else -self.length 73 | self.vertices[i + 1] = self.width if self.vertices[i + 1] > 0.0 else -self.width 74 | self.vertices[i + 2] = self.height if self.vertices[i + 2] > 0.0 else -self.height 75 | 76 | def update_length (self, val): 77 | self.length = val 78 | self.update() 79 | 80 | def update_width (self, val): 81 | self.width = val 82 | self.update() 83 | 84 | def update_height (self, val): 85 | self.height = val 86 | self.update() 87 | 88 | 89 | class Sphere(BaseGeometry): 90 | file_path = 'bunny.obj' 91 | 92 | def __init__ (self, val = None): 93 | super(Sphere, self).__init__(val) 94 | self.radius = 1.0 95 | self.stretch_rate = 1.0 96 | 97 | self.stretch_x = 1.0 98 | self.stretch_y = 1.0 99 | self.stretch_z = 1.0 100 | 101 | self.read(Sphere.file_path) 102 | 103 | def update_radius (self, val): 104 | self.radius = val 105 | self.stretch_rate = val / self.radius 106 | self.update() 107 | 108 | def update (self): 109 | self.stretch_rate = self.radius 110 | for i in range(0, len(self.vertices), 3): 111 | self.vertices[i] *= (self.stretch_rate * self.stretch_x) 112 | self.vertices[i + 1] *= (self.stretch_rate * self.stretch_y) 113 | self.vertices[i + 2] *= (self.stretch_rate * self.stretch_z) 114 | 115 | def update_stretch_x (self, val): 116 | self.stretch_x = val 117 | self.update() 118 | 119 | def update_stretch_y (self, val): 120 | self.stretch_y = val 121 | self.update() 122 | 123 | def update_stretch_z (self, val): 124 | self.stretch_z = val 125 | self.update() 126 | 127 | 128 | class Axis(BaseGeometry): 129 | def __init__ (self): 130 | super(Axis, self).__init__() 131 | self.vertices = np.array([[-5.0, 0.0, 5.0], # 0 132 | [5.0, 0.0, 5.0], # 1 133 | [5.0, 0.0, -5.0], # 2 134 | [-5.0, 0.0, -5.0]]) # 3 135 | self.indices = np.array([0, 1, 2, 0, 3, 2]) 136 | self.colors = np.array([[1.0, 0.0, 0.0], 137 | [0.0, 1.0, 0.0], 138 | [0.0, 0.0, 1.0], 139 | [1.0, 1.0, 1.0]]) 140 | -------------------------------------------------------------------------------- /_004_3d_loading_model_and_rotating/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from PyQt5.QtCore import QUrl 4 | from PyQt5.QtGui import QGuiApplication, QOpenGLVersionProfile, QSurfaceFormat 5 | from PyQt5.QtQuick import QQuickView 6 | from PyQt5.QtQml import qmlRegisterType 7 | 8 | from _004_3d_loading_model_and_rotating.object import ModelUnderlay 9 | import platform 10 | 11 | if __name__ == '__main__': 12 | # Not working in Ubuntu 16.04, result in 1282 error for simple calling like glViewport(...) 13 | # TODO 14 | 15 | if platform.uname().system == 'Darwin': 16 | f = QSurfaceFormat() 17 | f.setVersion(4, 1) 18 | f.setDepthBufferSize(1) # fix depth buffer error 19 | f.setStencilBufferSize(1) # fix stencil buffer error 20 | 21 | # If CoreProfile is used, all the other QML rendering will fail, because they only use 2.1 22 | f.setProfile(QSurfaceFormat.CompatibilityProfile) 23 | QSurfaceFormat.setDefaultFormat(f) 24 | 25 | qmlRegisterType(ModelUnderlay, 'OpenGLUnderQml', 1, 0, 'ModelUnderlay') 26 | 27 | app = QGuiApplication(sys.argv) 28 | 29 | view = QQuickView() 30 | # view.setFormat(f) 31 | view.setPersistentSceneGraph(True) 32 | view.setPersistentOpenGLContext(True) 33 | view.setResizeMode(QQuickView.SizeRootObjectToView) # Set for the object to resize correctly 34 | view.setSource(QUrl('ModelWindow.qml')) 35 | view.show() 36 | 37 | app.exec() 38 | -------------------------------------------------------------------------------- /_004_3d_loading_model_and_rotating/mesh/cube.obj: -------------------------------------------------------------------------------- 1 | # cube.obj 2 | # 3 | 4 | g cube 5 | 6 | v 0.0 0.0 0.0 7 | v 0.0 0.0 1.0 8 | v 0.0 1.0 0.0 9 | v 0.0 1.0 1.0 10 | v 1.0 0.0 0.0 11 | v 1.0 0.0 1.0 12 | v 1.0 1.0 0.0 13 | v 1.0 1.0 1.0 14 | 15 | vn 0.0 0.0 1.0 16 | vn 0.0 0.0 -1.0 17 | vn 0.0 1.0 0.0 18 | vn 0.0 -1.0 0.0 19 | vn 1.0 0.0 0.0 20 | vn -1.0 0.0 0.0 21 | 22 | f 1//2 7//2 5//2 23 | f 1//2 3//2 7//2 24 | f 1//6 4//6 3//6 25 | f 1//6 2//6 4//6 26 | f 3//3 8//3 7//3 27 | f 3//3 4//3 8//3 28 | f 5//5 7//5 8//5 29 | f 5//5 8//5 6//5 30 | f 1//4 5//4 6//4 31 | f 1//4 6//4 2//4 32 | f 2//1 6//1 8//1 33 | f 2//1 8//1 4//1 -------------------------------------------------------------------------------- /_004_3d_loading_model_and_rotating/qml/Button.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | //import OpenGLUnderQml 1.0 3 | import QtGraphicalEffects 1.0 4 | 5 | Rectangle { 6 | width: 100 7 | height: 50 8 | 9 | property alias text: text.text 10 | property alias mouse_area: mouse_area 11 | 12 | color: Qt.rgba(0.2, 0.2, 0.2, 1.0) 13 | 14 | Behavior on width { NumberAnimation { duration: 30 } } 15 | 16 | Text { 17 | id: text 18 | text: 'Button' 19 | anchors.centerIn: parent 20 | font.pointSize: 10 21 | color: 'white' 22 | } 23 | 24 | MouseArea { 25 | id: mouse_area 26 | 27 | anchors.fill: parent 28 | hoverEnabled: true 29 | 30 | onEntered: { 31 | parent.width = 120; 32 | parent.color = Qt.rgba(0.0, 0.2, 0.8, 1.0) 33 | } 34 | 35 | onExited: { 36 | parent.width = 100; 37 | parent.color = Qt.rgba(0.2, 0.2, 0.2, 1.0) 38 | } 39 | 40 | onClicked: { } 41 | } 42 | } -------------------------------------------------------------------------------- /_004_3d_loading_model_and_rotating/qml/ModelWindow.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import OpenGLUnderQml 1.0 3 | import QtGraphicalEffects 1.0 4 | 5 | Item { 6 | width: 640 7 | height: 480 8 | 9 | focus: true 10 | Keys.onPressed: { 11 | if (event.key == Qt.Key_W) { 12 | scene.move_camera(0); 13 | event.accepted = true; 14 | // console.log("(w) Move camera forward") 15 | } 16 | if (event.key == Qt.Key_S) { 17 | scene.move_camera(1); 18 | event.accepted = true; 19 | // console.log("(s) Move camera backward") 20 | } 21 | 22 | if (event.key == Qt.Key_A) { 23 | scene.move_camera(2); 24 | event.accepted = true; 25 | // console.log("(a) Move camera left") 26 | } 27 | 28 | if (event.key == Qt.Key_D) { 29 | scene.move_camera(3); 30 | event.accepted = true; 31 | // console.log("(d) Move camera right") 32 | } 33 | 34 | if (event.key == Qt.Key_O) { 35 | scene.move_camera(4); 36 | event.accepted = true; 37 | // console.log("(Space) Move camera ascend") 38 | } 39 | 40 | if (event.key == Qt.Key_P) { 41 | scene.move_camera(5); 42 | event.accepted = true; 43 | // console.log("(Space) Move camera ascend") 44 | } 45 | } 46 | 47 | // MouseArea { 48 | // onPositionChanged: { 49 | // console.log(mouse.x, mouse.y) 50 | // console.log('OK') 51 | // } 52 | // } 53 | 54 | ModelUnderlay { 55 | id: scene 56 | } 57 | 58 | Text { 59 | text: 'Simple 3D Editor By Kun' 60 | anchors { 61 | left: parent.left 62 | bottom: parent.bottom 63 | margins: 20 64 | } 65 | 66 | font.pointSize: 10 67 | color: 'white' 68 | } 69 | 70 | Button { 71 | id: add_cube_button 72 | text: 'Add Cube' 73 | anchors.top: parent.top 74 | anchors.right: parent.right 75 | 76 | mouse_area.onClicked: { 77 | scene.add_geometry(0) 78 | } 79 | } 80 | 81 | Button { 82 | id: delete_cube_button 83 | text: 'Delete Cube' 84 | anchors.top: add_cube_button.bottom 85 | anchors.right: parent.right 86 | 87 | mouse_area.onClicked: { 88 | scene.delete_geometry(0) 89 | } 90 | } 91 | 92 | Button { 93 | id: add_sphere_button 94 | text: 'Add Sphere' 95 | anchors.top: delete_cube_button.bottom 96 | anchors.right: parent.right 97 | 98 | mouse_area.onClicked: { 99 | scene.add_geometry(1) 100 | } 101 | } 102 | 103 | Button { 104 | id: delete_sphere_button 105 | text: 'Delete Sphere' 106 | anchors.top: add_sphere_button.bottom 107 | anchors.right: parent.right 108 | 109 | mouse_area.onClicked: { 110 | scene.delete_geometry(1) 111 | } 112 | } 113 | 114 | Button { 115 | id: stretch_x_button 116 | text: 'Stretch X' 117 | anchors.top: parent.top 118 | anchors.left: parent.left 119 | mouse_area.onClicked: { 120 | scene.stretch_x() 121 | } 122 | } 123 | 124 | Button { 125 | id: shrink_x_button 126 | text: 'Shrink X' 127 | anchors.top: parent.top 128 | anchors.left: stretch_x_button.right 129 | mouse_area.onClicked: { 130 | scene.shrink_x() 131 | } 132 | } 133 | 134 | Button { 135 | id: stretch_y_button 136 | text: 'Stretch Y' 137 | anchors.top: stretch_x_button.bottom 138 | anchors.left: parent.left 139 | mouse_area.onClicked: { 140 | scene.stretch_y() 141 | } 142 | } 143 | 144 | Button { 145 | id: shrink_y_button 146 | text: 'Shrink Y' 147 | anchors.top: shrink_x_button.bottom 148 | anchors.left: stretch_y_button.right 149 | mouse_area.onClicked: { 150 | scene.shrink_y() 151 | } 152 | } 153 | 154 | Button { 155 | id: stretch_z_button 156 | text: 'Stretch Z' 157 | anchors.top: stretch_y_button.bottom 158 | anchors.left: parent.left 159 | mouse_area.onClicked: { 160 | scene.stretch_z() 161 | } 162 | } 163 | 164 | Button { 165 | id: shrink_z_button 166 | text: 'Shrink Z' 167 | anchors.top: shrink_y_button.bottom 168 | anchors.left: stretch_z_button.right 169 | mouse_area.onClicked: { 170 | scene.shrink_z() 171 | } 172 | } 173 | 174 | Button { 175 | id: bigger_button 176 | text: 'Bigger' 177 | anchors.top: change_random_sphere_color.bottom 178 | anchors.left: parent.left 179 | mouse_area.onClicked: { 180 | scene.bigger_objects() 181 | } 182 | } 183 | 184 | Button { 185 | id: smaller_button 186 | text: 'Smaller' 187 | anchors.top: bigger_button.bottom 188 | anchors.left: parent.left 189 | mouse_area.onClicked: { 190 | scene.smaller_objects() 191 | } 192 | } 193 | 194 | 195 | Button { 196 | id: change_random_cube_color 197 | text:'Cube Color' 198 | anchors.top: stretch_z_button.bottom 199 | anchors.left: parent.left 200 | mouse_area.onClicked: { 201 | scene.change_random_cube_color() 202 | } 203 | } 204 | 205 | Button { 206 | id: change_random_sphere_color 207 | text:'Sphere Color' 208 | anchors.top: change_random_cube_color.bottom 209 | anchors.left: parent.left 210 | mouse_area.onClicked: { 211 | scene.change_random_sphere_color() 212 | } 213 | } 214 | 215 | 216 | } -------------------------------------------------------------------------------- /_004_3d_loading_model_and_rotating/render_engine.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QSize 2 | from PyQt5.QtQuick import QQuickItem 3 | from PyQt5.QtCore import Qt 4 | 5 | from PyQt5.QtGui import QOpenGLShaderProgram, QOpenGLShader, QMatrix4x4, QOpenGLContext 6 | from _004_3d_loading_model_and_rotating.utils import * 7 | from _004_3d_loading_model_and_rotating.geometries import Cube, Sphere, Axis 8 | 9 | import random 10 | import sys 11 | import numpy as np 12 | import platform as pf 13 | 14 | theta = 0.0 15 | 16 | if pf.uname().system == 'Linux': 17 | try: 18 | import OpenGL.GL as GL 19 | except ImportError as e: 20 | GL = None 21 | print('can\'t import OpenGL') 22 | sys.exit(1) 23 | 24 | 25 | class Entity(object): 26 | def __init__ (self, model, position, rotation, scale_val): 27 | self._model = model 28 | self.position = position 29 | self.rotation = rotation 30 | self.scale = scale_val 31 | 32 | 33 | class ModelUnderlay(QQuickItem): 34 | theta_changed = pyqtSignal(name = 'theta_changed') # the optional unbound notify signal. Probably no need herel 35 | 36 | def __init__ (self, parent = None): 37 | super(ModelUnderlay, self).__init__(parent) 38 | self._renderer = None 39 | self.windowChanged.connect(self.onWindowChanged) 40 | 41 | self._theta = 0.0 42 | 43 | # @pyqtSlot('QQuickWindow'), incompatible connection error, don't know why 44 | def onWindowChanged (self, window): 45 | # Because it's in different thread which required a direct connection 46 | # window == self.window(), they are pointing to the same window instance. Verified. 47 | window.beforeSynchronizing.connect(self.sync, type = Qt.DirectConnection) 48 | window.setClearBeforeRendering(False) # otherwise quick would clear everything we render 49 | 50 | @pyqtSlot(name = 'sync') 51 | def sync (self): 52 | if self._renderer is None: 53 | self._renderer = ModelUnderlayRenderer() 54 | self.window().beforeRendering.connect(self._renderer.paint, type = Qt.DirectConnection) 55 | self._renderer.set_viewport_size(self.window().size() * self.window().devicePixelRatio()) 56 | self._renderer.set_window(self.window()) 57 | self._renderer.set_projection_matrix() 58 | 59 | @pyqtSlot(int) 60 | def changeColor (self, color_enum): 61 | # if color_enum == 1: 62 | # colors = colors_red 63 | # elif color_enum == 2: 64 | # colors = colors_green 65 | # elif color_enum == 3: 66 | # colors = colors_blue 67 | # elif color_enum == 4: 68 | # colors = colors_mixed 69 | pass 70 | 71 | @pyqtSlot(int) 72 | def add_geometry (self, geo_enum): 73 | self._renderer.add_geometry(geo_enum) 74 | 75 | @pyqtSlot(int) 76 | def delete_geometry (self, index): 77 | self._renderer.delete_geometry(index) 78 | 79 | @pyqtSlot(int) 80 | def select_obj (self, index = 0): 81 | pass 82 | 83 | @pyqtSlot(float) 84 | def stretch_x (self, x): 85 | pass 86 | 87 | @pyqtSlot(float) 88 | def stretch_y (self, y): 89 | pass 90 | 91 | @pyqtSlot(float) 92 | def stretch_z (self, z): 93 | pass 94 | 95 | @pyqtSlot(int, int) 96 | def rotate_obj (self, x, y): 97 | pass 98 | 99 | @pyqtSlot(int, int) 100 | def rotate_camera (self, x, y): 101 | pass 102 | 103 | @pyqtSlot(int) 104 | def move_camera (self, key): 105 | """ 106 | Use keyboard to control camera 107 | :param key: 108 | :return: 109 | """ 110 | if key == 0: 111 | self._renderer.move_model(10) 112 | elif key == 1: 113 | self._renderer.move_model(-10) 114 | 115 | @pyqtSlot() 116 | def change_random_cube_color (self): 117 | self._renderer.change_random_cube_color() 118 | 119 | @pyqtSlot() 120 | def change_random_sphere_color (self): 121 | self._renderer.change_random_sphere_color() 122 | 123 | 124 | class ModelUnderlayRenderer(QObject): 125 | def __init__ (self, parent = None): 126 | super(ModelUnderlayRenderer, self).__init__(parent) 127 | 128 | self._cube_shader = None 129 | self._sphere_shader = None 130 | self._viewport_size = QSize() 131 | self._window = None 132 | self._camera = Camera() 133 | 134 | self._perspective_projection_matrix = None 135 | self._orthographic_projection_matrix = None 136 | 137 | self._model_matrix = np.identity(4) 138 | 139 | self._projection_type = 0 140 | self._projection_matrix = perspective_projection(45.0, 141 | 640.0 / 480.0, 142 | 0.001, 1000.0) 143 | 144 | self._index_buffer = -1 145 | 146 | # keep track of the objects in the scene 147 | self._cube_model = Cube() 148 | self._sphere_model = Sphere() 149 | 150 | self._models = dict() 151 | self._models[self._cube_model] = [] 152 | self._models[self._sphere_model] = [] 153 | 154 | @pyqtSlot() 155 | def paint (self): 156 | # for Darwin, it's a must 157 | if pf.uname().system == 'Darwin': 158 | global GL 159 | GL = self._window.openglContext().versionFunctions() 160 | 161 | w = self._viewport_size.width() 162 | h = self._viewport_size.height() 163 | 164 | GL.glViewport(0, 0, int(w), int(h)) 165 | GL.glClearColor(0.1, 0.1, 0.1, 1) 166 | GL.glEnable(GL.GL_DEPTH_TEST) 167 | GL.glClear(GL.GL_COLOR_BUFFER_BIT) 168 | GL.glClear(GL.GL_DEPTH_BUFFER_BIT) 169 | # 170 | # vertices_block = np.vstack((self._cube_model.vertices, self._sphere_model.vertices)) 171 | # colors_block = np.vstack((self._cube_model.colors, self._sphere_model)) 172 | # 173 | # if len(self._objects) > 1: 174 | # for v in self._vertices[1:]: 175 | # vertices_block = np.vstack((vertices_block, v)) 176 | # for idx, c in enumerate(self._colors[1:]): 177 | # if not c: 178 | # c = [[0.6, 0.6, 0.7] for i in range(len(self._vertices[idx]))] 179 | # colors_block = np.vstack((colors_block, c)) 180 | # for i in self._indices[1:]: 181 | # indices_block = np.hstack((indices_block, i)) 182 | view_matrix = np.identity(4) 183 | view_matrix[2][3] = -30 184 | 185 | if self._cube_shader is None: 186 | self._cube_shader = QOpenGLShaderProgram() 187 | self._cube_shader.addShaderFromSourceFile(QOpenGLShader.Vertex, 'shaders/OpenGL_2_1/vertex.glsl') 188 | self._cube_shader.addShaderFromSourceFile(QOpenGLShader.Fragment, 'shaders/OpenGL_2_1/fragment.glsl') 189 | self._cube_shader.bindAttributeLocation('position', 0) 190 | self._cube_shader.bindAttributeLocation('color', 1) 191 | self._cube_shader.link() 192 | 193 | self._cube_shader.bind() 194 | self._cube_shader.enableAttributeArray(0) 195 | self._cube_shader.enableAttributeArray(1) 196 | self._cube_shader.setAttributeArray(0, self._cube_model.vertices.tolist()) 197 | self._cube_shader.setAttributeArray(1, self._cube_model.colors.tolist()) 198 | # view_matrix = self._camera.get_view_matrix() 199 | self._cube_shader.setUniformValue('view_matrix', 200 | QMatrix4x4(view_matrix.flatten().tolist()).transposed()) 201 | self._cube_shader.setUniformValue('projection_matrix', 202 | QMatrix4x4(self._projection_matrix.flatten().tolist()).transposed()) 203 | 204 | if self._cube_model in self._models.keys(): 205 | for entity in self._models[self._cube_model]: 206 | m = create_transformation_matrix(entity.position, entity.rotation, entity.scale) 207 | self._cube_shader.setUniformValue('model_matrix', QMatrix4x4(m.flatten().tolist())) 208 | GL.glDrawElements(GL.GL_TRIANGLES, 209 | len(self._cube_model.indices), 210 | GL.GL_UNSIGNED_INT, 211 | self._cube_model.indices.tolist()) 212 | 213 | self._cube_shader.disableAttributeArray(0) 214 | self._cube_shader.disableAttributeArray(1) 215 | self._cube_shader.release() 216 | 217 | if self._sphere_shader is None: 218 | self._sphere_shader = QOpenGLShaderProgram() 219 | self._sphere_shader.addShaderFromSourceFile(QOpenGLShader.Vertex, 'shaders/OpenGL_2_1/vertex.glsl') 220 | self._sphere_shader.addShaderFromSourceFile(QOpenGLShader.Fragment, 'shaders/OpenGL_2_1/fragment.glsl') 221 | self._sphere_shader.bindAttributeLocation('position', 0) 222 | self._sphere_shader.bindAttributeLocation('color', 1) 223 | self._sphere_shader.link() 224 | 225 | self._sphere_shader.bind() 226 | self._sphere_shader.enableAttributeArray(0) 227 | self._sphere_shader.enableAttributeArray(1) 228 | self._sphere_shader.setAttributeArray(0, self._sphere_model.vertices.tolist()) 229 | self._sphere_shader.setAttributeArray(1, self._sphere_model.colors.tolist()) 230 | self._sphere_shader.setUniformValue('view_matrix', 231 | QMatrix4x4(view_matrix.flatten().tolist()).transposed()) 232 | self._sphere_shader.setUniformValue('projection_matrix', 233 | QMatrix4x4(self._projection_matrix.flatten().tolist()).transposed()) 234 | 235 | if self._sphere_model in self._models.keys(): 236 | for entity in self._models[self._sphere_model]: 237 | m = create_transformation_matrix(entity.position, entity.rotation, entity.scale) 238 | self._cube_shader.setUniformValue('model_matrix', QMatrix4x4(m.flatten().tolist())) 239 | GL.glDrawElements(GL.GL_TRIANGLES, 240 | len(self._sphere_model.indices), 241 | GL.GL_UNSIGNED_INT, 242 | self._sphere_model.indices.tolist()) 243 | self._sphere_shader.disableAttributeArray(0) 244 | self._sphere_shader.disableAttributeArray(1) 245 | self._sphere_shader.release() 246 | 247 | # def build_rotation_matrix (t): 248 | # m = np.identity(4) 249 | # m[0][0] = np.cos(np.radians(t)) 250 | # m[0][2] = np.sin(np.radians(t)) 251 | # m[2][0] = -np.sin(np.radians(t)) 252 | # m[2][2] = np.cos(np.radians(t)) 253 | # return m 254 | # 255 | # global theta 256 | # theta += 1 257 | # self._model_matrix = build_rotation_matrix(theta) 258 | # self._model_matrix[2][3] = -3 259 | 260 | 261 | # Restore the OpenGL state for QtQuick rendering 262 | self._window.resetOpenGLState() 263 | self._window.update() 264 | 265 | def set_viewport_size (self, size): 266 | self._viewport_size = size 267 | 268 | def set_window (self, window): 269 | self._window = window 270 | 271 | def set_projection_matrix (self): 272 | # Need to be set every time we change the size of the window 273 | self._projection_matrix = perspective_projection(45.0, 274 | self._window.width() / self._window.height(), 275 | 0.001, 1000.0) 276 | 277 | def move_model (self, val): 278 | self._model_matrix[2][3] += val 279 | 280 | def move_camera (self): 281 | pass 282 | 283 | def add_geometry (self, geo_enum): 284 | if geo_enum == 0: 285 | self._models[self._cube_model].append(Entity(self._cube_model, 286 | np.array([random.uniform(-3.0, 3.0), 287 | random.uniform(-3.0, 3.0), 288 | random.uniform(-20.0, -10.0)]), 289 | np.array([random.uniform(-45.0, 45.0), 290 | random.uniform(-45.0, 45.0), 291 | random.uniform(-45.0, 45.0)]), 292 | np.array([1.0, 1.0, 1.0]))) 293 | elif geo_enum == 1: 294 | self._models[self._sphere_model].append(Entity(self._sphere_model, 295 | np.array([random.uniform(-3.0, 3.0), 296 | random.uniform(-3.0, 3.0), 297 | random.uniform(-20.0, -10.0)]), 298 | np.array([random.uniform(-30.0, 30.0), 299 | random.uniform(-30.0, 30.0), 300 | random.uniform(-30.0, 30.0)]), 301 | np.array([1.0, 1.0, 1.0]))) 302 | else: 303 | return 304 | 305 | def delete_geometry (self, geo_enum): 306 | if geo_enum == 0: 307 | if self._models[self._cube_model]: 308 | self._models[self._cube_model].pop() 309 | elif geo_enum == 1: 310 | if self._models[self._sphere_model]: 311 | self._models[self._sphere_model].pop() 312 | 313 | def change_random_cube_color (self): 314 | tmp = self._models[self._cube_model] 315 | self._models.pop(self._cube_model) 316 | self._cube_model = Cube() 317 | self._models[self._cube_model] = tmp 318 | 319 | def change_random_sphere_color (self): 320 | tmp = self._models[self._sphere_model] 321 | self._models.pop(self._sphere_model) 322 | self._sphere_model = Sphere() 323 | self._models[self._sphere_model] = tmp 324 | -------------------------------------------------------------------------------- /_004_3d_loading_model_and_rotating/shaders/OpenGL_2_1/fragment.glsl: -------------------------------------------------------------------------------- 1 | varying highp vec3 pass_color; 2 | 3 | void main () { 4 | gl_FragColor = vec4(pass_color, 1.0); 5 | } 6 | -------------------------------------------------------------------------------- /_004_3d_loading_model_and_rotating/shaders/OpenGL_2_1/vertex.glsl: -------------------------------------------------------------------------------- 1 | attribute highp vec3 position; 2 | attribute highp vec3 color; 3 | uniform highp mat4 model_matrix; 4 | uniform highp mat4 view_matrix; 5 | uniform highp mat4 projection_matrix; 6 | 7 | varying vec3 pass_color; 8 | 9 | void main () { 10 | gl_Position = projection_matrix * view_matrix * model_matrix * vec4(position, 1.0); 11 | pass_color = color; 12 | } 13 | -------------------------------------------------------------------------------- /_004_3d_loading_model_and_rotating/shaders/OpenGL_4_1/fragment.glsl: -------------------------------------------------------------------------------- 1 | #version 410 2 | 3 | in vec3 pass_color; 4 | 5 | out vec4 out_color; 6 | 7 | void main () { 8 | out_color = vec4(pass_color, 1.0); 9 | } 10 | -------------------------------------------------------------------------------- /_004_3d_loading_model_and_rotating/shaders/OpenGL_4_1/vertex.glsl: -------------------------------------------------------------------------------- 1 | #version 410 2 | 3 | layout (location = 0) in vec3 position; 4 | layout (location = 1) in vec3 color; 5 | 6 | uniform mat4 model_matrix; 7 | uniform mat4 view_matrix; 8 | uniform mat4 projection_matrix; 9 | 10 | out vec3 pass_color; 11 | 12 | void main () { 13 | mat4 m = projection_matrix * view_matrix * model_matrix; 14 | gl_Position = m * vec4(position, 1.0); 15 | pass_color = color; 16 | } 17 | -------------------------------------------------------------------------------- /_004_3d_loading_model_and_rotating/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import numpy.linalg as la 3 | from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty 4 | from PyQt5.QtQml import QQmlListProperty 5 | import math 6 | 7 | 8 | class Camera(object): 9 | def __init__ (self, parent = None): 10 | self._eye = np.array([0.0, 0.0, -30.0]) 11 | self._up = np.array([0.0, 1.0, 0.0]) 12 | self._target = np.array([0.0, 0.0, 1.0]) 13 | self.x = None 14 | self.y = None 15 | self.z = None 16 | 17 | self.mouse_x = 0.0 18 | self.mouse_y = 0.0 19 | self._m = np.identity(4) 20 | self.update_view_matrix() 21 | 22 | def get_view_matrix (self): 23 | # m = np.identity(4) 24 | # m[2][3] = -60.0 25 | # return m 26 | 27 | return self._m 28 | 29 | def get_projection_matrix (self): 30 | pass 31 | 32 | def update_view_matrix (self): 33 | # self.z = normalize_vector(self._eye - self._target) # positive z is pointing at screen 34 | # self.x = normalize_vector(np.cross(self._up, self.z)) 35 | # self.y = normalize_vector(np.cross(self.z, self.x)) 36 | # 37 | # self._m[:, 0] = np.array([self.x[0], self.x[1], self.x[2], 0.0]) 38 | # self._m[:, 1] = np.array([self.y[0], self.y[1], self.y[2], 0.0]) 39 | # self._m[:, 2] = np.array([self.z[0], self.z[1], self.z[2], 0.0]) 40 | # self._m[:, 3] = np.array([self._eye[0], self._eye[1], self._eye[2], 1.0]) 41 | self._m = look_at(self._eye, self._eye + self._target, self._up) 42 | # print(self._m) 43 | 44 | @pyqtSlot(float) 45 | def move_horizontally (self, dist): 46 | self._eye += dist * normalize_vector(self.x) 47 | self._target += dist * normalize_vector(self.x) 48 | self.update_view_matrix() 49 | 50 | @pyqtSlot(float) 51 | def move_vertically (self, dist): 52 | self._eye += dist * normalize_vector(self.y) 53 | self._target += dist * normalize_vector(self.y) 54 | self.update_view_matrix() 55 | 56 | @pyqtSlot(float) 57 | def move_forward (self, dist): 58 | self._eye += dist * normalize_vector(self.z) 59 | self._target += dist * normalize_vector(self.z) 60 | self.update_view_matrix() 61 | 62 | @pyqtSlot() 63 | def rotate_horizontally (self, angle): 64 | pass 65 | 66 | @pyqtSlot() 67 | def rotate_vertically (self, angle): 68 | pass 69 | 70 | 71 | def perspective_projection (fovy, aspect_ratio, near_z, far_z): 72 | m = np.zeros(shape = (4, 4)) 73 | 74 | t = np.tan(np.radians(fovy) / 2.0) # half width 75 | 76 | m[0][0] = 1.0 / (aspect_ratio * t) 77 | m[1][1] = 1.0 / t 78 | m[2][2] = -(far_z + near_z) / (far_z - near_z) 79 | m[2][3] = -1.0 80 | m[3][2] = -(2.0 * far_z * near_z) / (far_z - near_z) 81 | 82 | return m 83 | 84 | 85 | def look_at (eye, center, up): 86 | f = normalize_vector(center - eye) 87 | u = normalize_vector(up) 88 | s = normalize_vector(np.cross(f, u)) 89 | u = np.cross(s, f) 90 | 91 | m = np.identity(4) 92 | 93 | m[0, :] = np.array([s[0], s[1], s[2], -np.dot(s, eye)]) 94 | m[1, :] = np.array([u[0], u[1], u[2], -np.dot(u, eye)]) 95 | m[2, :] = np.array([-f[0], -f[1], -f[2], np.dot(f, eye)]) 96 | 97 | return m 98 | 99 | 100 | # assuming the volume is symmetric such that no need to specify l, r, t, b 101 | # Tested against glm::ortho(l, r, b, t, near_z, far_z) with 102 | # glm::mat4 p = glm::ortho(-320.0f, 320.0f, -240.0f, 240.0f, 0.001f, 100.0f); 103 | # TODO should be farther tested 104 | def orthographic_projection (w, h, near_z, far_z): 105 | m = np.zeros(shape = (4, 4)) 106 | 107 | m[0][0] = 2.0 / w 108 | m[1][1] = 2.0 / h 109 | m[2][2] = -2.0 / (far_z - near_z) 110 | m[2][3] = -(far_z + near_z) / (far_z - near_z) 111 | m[3][3] = 1 112 | 113 | return m 114 | 115 | 116 | def normalize_vector (v): 117 | return v / la.norm(v) 118 | 119 | 120 | def rotate_x (angle): 121 | rad = np.radians(angle) 122 | m = np.identity(4) 123 | m[0][0] = 1.0 124 | m[1][1] = np.cos(rad) 125 | m[1][2] = -np.sin(rad) 126 | m[2][1] = np.sin(rad) 127 | m[2][2] = np.cos(rad) 128 | return m 129 | 130 | 131 | def rotate_y (angle): 132 | rad = np.radians(angle) 133 | m = np.identity(4) 134 | m[0][0] = np.cos(rad) 135 | m[0][2] = np.sin(rad) 136 | m[1][1] = 1.0 137 | m[2][0] = -np.sin(rad) 138 | m[2][2] = np.cos(rad) 139 | return m 140 | 141 | 142 | def rotate_z (angle): 143 | rad = np.radians(angle) 144 | m = np.identity(4) 145 | m[0][0] = np.cos(rad) 146 | m[0][1] = -np.sin(rad) 147 | m[1][0] = np.sin(rad) 148 | m[1][1] = np.cos(rad) 149 | m[2][2] = 1.0 150 | return m 151 | 152 | 153 | def translate (trans): 154 | m = np.identity(4) 155 | m[0][3] = trans[0] 156 | m[1][3] = trans[1] 157 | m[2][3] = trans[2] 158 | return m 159 | 160 | 161 | def scale (scale_vec): 162 | m = np.identity(4) 163 | m[0][0] = scale_vec[0] 164 | m[1][1] = scale_vec[1] 165 | m[2][2] = scale_vec[2] 166 | return m 167 | 168 | 169 | def create_transformation_matrix (translation, 170 | rotation, 171 | scale_val): 172 | m = np.identity(4) 173 | m2 = rotate_x(rotation[0]) 174 | m3 = rotate_y(rotation[1]) 175 | m4 = rotate_z(rotation[2]) 176 | m5 = scale(scale_val) 177 | 178 | m = m5 * m4 * m3 * m2 * m 179 | 180 | m[0][3] = translation[0] 181 | m[1][3] = translation[1] 182 | m[2][3] = translation[2] 183 | # m[0][3] = 0 184 | # m[1][3] = 0 185 | # m[2][3] = 30 186 | 187 | return m 188 | --------------------------------------------------------------------------------