摘要:介紹Python和OpenGL的入門知識,包括安裝、語法、基本圖形繪製等。
本文分享自華為雲社群《[Python影像處理] 二十七.OpenGL入門及繪製基本圖形(一)》,作者:eastmount。
一.OpenGL入門知識
1.什麼是OpenGL
OpenGL(Open Graphics Library,譯為“開放式圖形庫”) 是用於渲染2D、3D向量圖形的跨語言、跨平臺的應用程式程式設計介面(API)。這個介面由近350個不同的函式呼叫組成,用來繪製從簡單的圖形元件到複雜的三維景象。OpenGL常用於CAD、虛擬現實、科學視覺化程式和電子遊戲開發。
OpenGL可用於設定所需的物件、影像和操作,以便開發互動式的3維計算機圖形應用程式。OpenGL被設計為一個現代化的、硬體無關的介面,因此我們可以在不考慮計算機作業系統或視窗系統的前提下,在多種不同的圖形硬體系統上,或者完全透過軟體的方式實現OpenGL的介面。OpenGL的高效實現(利用了圖形加速硬體)存在於Windows,部分UNIX平臺和Mac OS。這些實現一般由顯示裝置廠商提供,而且非常依賴於該廠商提供的硬體。
OpenGL規範由1992年成立的OpenGL架構評審委員會(ARB)維護。ARB由一些對建立一個統一的、普遍可用的API特別感興趣的公司組成。到了今天已經發布了非常多的OpenGL版本,以及大量構建於OpenGL之上以簡化應用程式開發過程的軟體庫。這些軟體庫大量用於影片遊戲、科學視覺化和醫學軟體的開發,或者只是用來顯示影像。
一個用來渲染影像的OpenGL程式需要執行的主要操作如下:
- 從OpenGL的幾何圖元中設定資料,用於構建形狀
- 使用不同的著色器(shader)對輸入的圖後設資料執行計算操作,判斷它們的位置、顏色,以及其他渲染屬性
- 將輸入圖元的數學描述轉換為與螢幕位置對應的畫素片元(fragment),這一步也稱作光柵化(rasterization)
- 最後,針對光柵化過程產生的每個片元,執行片元著色器(fragment shader),從而決定這個片元的最終顏色和位置
- 如果有必要,還需要對每個片元執行一些額外的操作,例如判斷片元對應的物件是否可見,或者將片元的顏色與當前螢幕位置的顏色進行融合
2.OpenGL安裝
作者的電腦環境為Win10+Python3.7,開啟CMD呼叫pip工具進行安裝,如下圖所示。
cd C:\Software\Program Software\Python37\Scripts
pip install pyopengl
但通常安裝成功之後,執行程式碼會報錯“OpenGL.error.NullFunctionError: Attempt to call an undefined function glutInit, check for bool(glutInit) before calling”。
據說是pip預設安裝的是32位版本的pyopengl,而作者的作業系統是64位。網上很多大牛會去 “https://www.lfd.uci.edu/~gohlke/pythonlibs/#pyopengl” 網站下載適合自己的版本。比如Python3.7且64位作業系統。
安裝流程如下所示:
pip install D:\PyOpenGL-3.1.5-cp37-cp37m-win_amd64.whl pip install D:\PyOpenGL-3.1.5-cp37-cp37m-win32.whl
寫到這裡,我們Python的OpenGL庫就安裝成功了!
二.OpenGL入門程式
我們首先介紹兩個入門程式碼,然後再進行深入的講解。
1.OpenGL繪製正方形
完整程式碼如下:
# -*- coding: utf-8 -*- from OpenGL.GL import * from OpenGL.GLU import * from OpenGL.GLUT import * # 繪製影像函式 def display(): # 清除螢幕及深度快取 glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT) # 設定紅色 glColor3f(1.0, 0.0, 0.0) # 開始繪製四邊形 glBegin(GL_QUADS) # 繪製四個頂點 glVertex3f(-0.5, -0.5, 0.0) glVertex3f(0.5, -0.5, 0.0) glVertex3f(0.5, 0.5, 0.0) glVertex3f(-0.5, 0.5, 0.0) # 結束繪製四邊形 glEnd() # 清空緩衝區並將指令送往硬體執行 glFlush() # 主函式 if __name__ == "__main__": # 使用glut庫初始化OpenGL glutInit() # 顯示模式 GLUT_SINGLE無緩衝直接顯示|GLUT_RGBA採用RGB(A非alpha) glutInitDisplayMode(GLUT_SINGLE | GLUT_RGBA) # 設定視窗位置大小 glutInitWindowSize(400, 400) # 建立視窗 glutCreateWindow("eastmount") # 呼叫display()函式繪製影像 glutDisplayFunc(display) # 進入glut主迴圈 glutMainLoop()
執行結果如下圖所示:
核心步驟如下:
- 主函式使用glut庫初始化OpenGL
glutInit() - 設定顯示模式並初始化glut視窗(畫布)
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGBA)
glutInitWindowSize(400, 400)
glutCreateWindow(“eastmount”) - 註冊繪製影像的回撥函式,如display()
glutDisplayFunc(display) - 繪製影像display函式,包括清除畫布、設定顏色、繪製圖元、設定定點、結束繪製、重新整理執行
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
glColor3f(1.0, 0.0, 0.0)
glBegin(GL_QUADS)
glVertex3f(-0.5, -0.5, 0.0)
glVertex3f(0.5, -0.5, 0.0)
glVertex3f(0.5, 0.5, 0.0)
glVertex3f(-0.5, 0.5, 0.0)
glEnd()
glFlush() - 進入glut主迴圈
glutMainLoop()
2.OpenGL繪製水壺
接著補充一段經典的水壺程式碼,所有計算機試卷、計算機圖形學、3D影像領域都會繪製它。
# -*- coding: utf-8 -*- from OpenGL.GL import * from OpenGL.GLU import * from OpenGL.GLUT import * # 繪製影像函式 def drawFunc(): # 清除螢幕及深度快取 glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT) # 設定繞軸旋轉(角度,x,y,z) glRotatef(0.1, 5, 5, 0) # 繪製實心茶壺 # glutSolidTeapot(0.5) # 繪製線框茶壺 glutWireTeapot(0.5) # 重新整理顯示影像 glFlush() # 主函式 if __name__ == "__main__": # 使用glut庫初始化OpenGL glutInit() # 顯示模式 GLUT_SINGLE無緩衝直接顯示|GLUT_RGBA採用RGB(A非alpha) glutInitDisplayMode(GLUT_SINGLE | GLUT_RGBA) # 設定視窗位置及大小 glutInitWindowPosition(0, 0) glutInitWindowSize(400, 400) # 建立視窗 glutCreateWindow("CSDN Eastmount") # 呼叫display()函式繪製影像 glutDisplayFunc(drawFunc) # 設定全域性的回撥函式 # 當沒有視窗事件到達時,GLUT程式功能可以執行後臺處理任務或連續動畫 glutIdleFunc(drawFunc) # 進入glut主迴圈 glutMainLoop()
執行結果如下圖所示,它主要呼叫glutSolidTeapot(0.5)函式繪製實現實心茶壺,glutWireTeapot(0.5)函式繪製線框茶壺。
注意,glut提供了一些現成的繪製立體的API,如glutWireSphere繪製球、glutWireCone繪製椎體、glutWireCube繪製立體、glutWireTorus繪製甜圈、glutWireTeapot繪製茶壺、glutWireOctahedron繪製八面體,請讀者自行提升。
3.OpenGL繪製多個圖形
接下來繪製一個座標系,並分別繪製四個圖形,設定不同顏色,程式碼如下所示。
# -*- coding: utf-8 -*- from OpenGL.GL import * from OpenGL.GLU import * from OpenGL.GLUT import * # 繪製影像函式 def display(): # 清除螢幕及深度快取 glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT) # 繪製線段 glBegin(GL_LINES) glVertex2f(-1.0, 0.0) # 左下角頂點 glVertex2f(1.0, 0.0) # 右下角頂點 glVertex2f(0.0, 1.0) # 右上角頂點 glVertex2f(0.0, -1.0) # 左上角頂點 glEnd() # 繪製頂點 glPointSize(10.0) glBegin(GL_POINTS) glColor3f(1.0, 0.0, 0.0) # 紅色 glVertex2f(0.3, 0.3) glColor3f(0.0, 1.0, 0.0) # 綠色 glVertex2f(0.5, 0.6) glColor3f(0.0, 0.0, 1.0) # 藍色 glVertex2f(0.9, 0.9) glEnd() # 繪製四邊形 glColor3f(1.0, 1.0, 0) glBegin(GL_QUADS) glVertex2f(-0.2, 0.2) glVertex2f(-0.2, 0.5) glVertex2f(-0.5, 0.5) glVertex2f(-0.5, 0.2) glEnd() # 繪製多邊形 glColor3f(0.0, 1.0, 1.0) glPolygonMode(GL_FRONT, GL_LINE) glPolygonMode(GL_BACK, GL_FILL) glBegin(GL_POLYGON) glVertex2f(-0.5, -0.1) glVertex2f(-0.8, -0.3) glVertex2f(-0.8, -0.6) glVertex2f(-0.5, -0.8) glVertex2f(-0.2, -0.6) glVertex2f(-0.2, -0.3) glEnd() # 繪製三角形 glColor3f(1.0, 1.0, 1.0) glPolygonMode(GL_FRONT, GL_FILL) glPolygonMode(GL_BACK, GL_LINE) glBegin(GL_TRIANGLES) glVertex2f(0.5, -0.5) glVertex2f(0.3, -0.3) glVertex2f(0.2, -0.6) # 結束繪製四邊形 glEnd() # 清空緩衝區並將指令送往硬體執行 glFlush() # 主函式 if __name__ == "__main__": # 使用glut庫初始化OpenGL glutInit() # 顯示模式 GLUT_SINGLE無緩衝直接顯示|GLUT_RGBA採用RGB(A非alpha) glutInitDisplayMode(GLUT_SINGLE | GLUT_RGBA) # 設定視窗位置及大小 glutInitWindowSize(400, 400) glutInitWindowPosition(500, 300) # 建立視窗 glutCreateWindow("CSDN Eastmount") # 呼叫display()函式繪製影像 glutDisplayFunc(display) # 進入glut主迴圈 glutMainLoop()
輸出結果如下圖所示:
4.OpenGL繪圖程式碼及原理詳解
該部分將詳細講解上面三段程式碼的核心知識,幫助大家鞏固基礎。作者讓大家先看程式碼及其執行效果,從而提升OpenGL程式設計興趣,再深入分析其原理,這種倒敘的方式希望您們喜歡。
(1) 核心函式
上述程式碼中,以glut開頭的函式都是GLUT工具包所提供的函式。
- glutInit():對GLUT進行初始化,該函式必須在其它的GLUT使用之前呼叫一次。其格式比較死板,一般glutInit()直接呼叫即可。
- glutInitDisplayMode():設定顯示方式,其中GLUT_RGB表示使用RGB顏色,與之對應的是GLUT_INDEX(表示使用索引顏色);GLUT_SINGLE表示使用單緩衝,與之對應的是GLUT_DOUBLE(表示使用雙緩衝)。更多引數請讀者閱讀官方網站或Google。
- glutInitWindowPosition():設定視窗在螢幕中的位置。
- glutInitWindowSize():設定視窗的大小,兩個參數列示長度和寬度。
- glutCreateWindow():根據當前設定的資訊建立視窗,引數將作為視窗的標題。需要注意的是,當視窗被建立後,並不是立即顯示到螢幕上,需要呼叫glutMainLoop()才能看到視窗。
- glutDisplayFunc():設定一個函式,當需要進行畫圖時,這個函式就會被呼叫,通常用來呼叫繪製圖形函式。
- glutMainLoop():進行一個訊息迴圈,大家需要知道這個函式可以顯示視窗,並且等待視窗關閉後才會返回。
以gl開頭的函式都是OpenGL的標準函式。
- glClear():清除,其中引數GL_COLOR_BUFFER_BIT表示清除顏色,GL_DEPTH_BUFFER_BIT表示清除深度。
- glRectf():畫一個矩形,四個引數分別表示位於對角線上的兩個點的橫、縱座標。
- glFlush():重新整理顯示影像,保證前面的OpenGL命令立即執行,而不是讓它們在緩衝區中等待。
- OpenGL要求指定頂點的命令(glVertex2f)必須包含在glBegin()函式和glEnd()函式之間執行。
(2) 繪製頂點
頂點(vertex)是 OpengGL 中非常重要的概念,描述線段、多邊形都離不開頂點。它們都是以glVertex開頭,後面跟一個數字和1~2個字母,比如:
- glVertex2d
- glVertex2f
- glVertex3f
- glVertex3fv
數字表示引數的個數,2表示有2個引數(xy座標),3表示三個(xyz座標),4表示四個(齊次座標 w)。字母表示引數的型別,s表示16位整數(OpenGL中將這個型別定義為GLshort),i表示32位整數(OpenGL中將這個型別定義為GLint和GLsizei),f表示32為浮點數(OpenGL中將這個型別定義為GLfloat和GLclampf),d表示64位浮點數(OpenGL中將這個型別定義為GLdouble和GLclampd)。例如:
- glVertex2i(1, 3)
- glVertex2f(1.0, 3.0)
- glVertex3f(1.0, 3.0, 1.0)
- glVertex4f(1.0, 3.0, 0.0, 1.0)
注意,OpenGL中很多函式都採用這種形式命名。
(3) 設定顏色
在OpenGL中,設定顏色函式以glColor開頭,後面跟著引數個數和引數型別。引數可以是0到255之間的無符號整數,也可以是0到1之間的浮點數。三個引數分別表示RGB分量,第四個參數列示透明度(其實叫不透明度更恰當)。以下最常用的兩個設定顏色的方法:
- glColor3f(1.0,0.0,0.0) #紅色
- glColor3f(0.0,1.0,0.0) #綠色
- glColor3f(0.0,0.0,1.0) #藍色
- glColor3f(1.0,1.0,1.0) #白色
- glColor4f(0.0,1.0,0.0,0.0) #紅色且不透明度
- glColor3ub(255, 0, 0) #紅色
注意,OpenGL是使用狀態機模式,顏色是一個狀態變數,設定顏色就是改變這個狀態變數並一直生效,直到再次呼叫設定顏色的函式。除了顏色,OpenGL 還有很多的狀態變數或模式。
(4) 繪製基本圖形
前面我們介紹了各種影像,下表展示了常見的影像元件。
- GL_POINTS:繪製頂點
- GL_LINES:繪製線段
- GL_LINE_STRIP:繪製連續線段
- GL_LINE_LOOP:繪製閉合的線段
- GL_POLYGON:繪製多邊形
- GL_TRIANGLES:繪製三角形
- GL_TRIANGLE_STRIP:繪製連續三角形
- GL_TRIANGLE_FAN:繪製多個三角形組成的扇形
- GL_QUADS:繪製四邊形
- GL_QUAD_STRIP:繪製連續四邊形
詳見下圖所示。
三.OpenGL基礎知識
在深入學習OpenGL之前,我們有必要了解一些最常用的圖形學名詞、OpenGL原理和語法。
1.OpenGL語法
OpenGL程式的基本結構通常包括——初始化物體渲染所對應的狀態、設定需要渲染的物體。渲染(render)表示計算機從模型建立最終影像的過程,OpenGL只是其中一種渲染系統。模型(model)或者場景物件是透過幾何圖元,比如點、線和三角形來構建的,而圖元與模型的頂點(vertex)也存在著各種對應的關係。
OpenGL另一個最本質的概念叫著色器,它是圖形硬體裝置所執行的一類特色函式。可以將著色器理解為專為圖形處理單元(GPU)編譯的一種小型程式。在OpenGL中,會用到始終不同的著色階段(shader stage),最常用的包括頂點著色器(vertex shader)以及片元著色器,前者用於處理頂點資料,後者用於處理光柵化後的片後設資料。所有的OpenGL程式都需要用到這兩類著色器。最終生成的影像包含了螢幕上繪製的所有畫素點。畫素(pixel)是顯示器上最小的可見單元。計算機系統將所有的畫素儲存到幀快取(framebuffer)當中,後者是由圖形硬體裝置管理的一塊獨立記憶體區域,可以直接對映到最終的顯示裝置上。
正如前面您看到的,OpenGL庫中所有的函式都會以字元“gl”作為字首,然後是一個或者多個大寫字母開頭的片語,以此來命令一個完整的函式(例如glBindVertexArray())。OpenGL的所有函式都是這種格式,上面看到的“glut”開頭的函式,它們來自第三方庫OpenGL Utility Toolkit(GLUT),可以用來顯示視窗、管理使用者輸入以及執行其他一些操作。
與函式命名約定類似,OpenGL庫中定義的常量也是GL_COLOR_BUFFER_BIT的形式,常量以GL_作為字首,並且使用下劃線來分割單詞。這些常量的定義是透過#define來完成的,它們基本可以在OpenGL的標頭檔案glcorearb.h和glext.h中找到。
為了能夠方便地在不同的作業系統之間移植OpenGL程式,它還為函式定義了不同的資料型別,例如GLfloat是浮點數型別。此外,比如glVertex*()的函式,它有多種變化形式,如glVertex2d、glVertex2f。在函式名稱的“核心”部分之後,透過字尾的變化來提示函式應當傳入的引數,通常由一個數字和1~2個字母組成。glVertex2f()中的“2”表示需要傳入2個引數,f表示浮點數。
2.老式OpenGL vs 現代OpenGL
(1) 老式OpenGL
在大多數計算機圖形系統中,繪圖的方式是將一些頂點傳送給處理管線,管線由一系列功能模組互相連線而成。最近,OpenGL應用程式設計介面(API)從固定功能的圖形管線轉換為可程式設計的圖形管線。
如下圖繪製正方形的程式碼,它使用的是老式OpenGL,要為三維圖元(在這個程式碼中,是一個GL_QUADS即矩形)指定各個頂點,但隨後每個頂點需要被分別傳送到GPU,這是低效的方式。 這種老式程式設計模式伸縮性不好,如果幾何圖形變得複雜,程式就會很慢。對於螢幕上的頂點和畫素如何變換,它只提供了有限的控制。
後續我們將專注於現代的OpenGL,但是網路上也會有老式OpenGL的例子。
(2) 現代OpenGL
現代OpenGL利用一系列的操作,即透過“三維圖形管線”繪製圖形,其基本流程如下圖所示。
簡化三維圖形管線分為6步:
- 三維幾何圖形定義(VBO等)。 在第一步,透過定義在三維空間中的三角形的頂點,並指定每個頂點相關聯的顏色,我們定義了三維幾何圖形。
- 頂點著色器。 接下來,變換這些頂點:第一次變換將這些頂點放在三維空間中,第二次變換將三維座標投影到二維空間。根據照明等因素,對應頂點的顏色值也在這一步中計算,這在程式碼中通常稱為“頂點著色器”。
- 光柵化。 將幾何圖形“光柵化”(從幾何物體轉換為畫素)。
- 片段著色器。 針對每個畫素,執行另一個名為“片段著色器”的程式碼塊。正如頂點著色器作用於三維頂點,片段著色器作用於光柵化後的二維畫素。
- 幀緩衝區操作(深度測試、混合等)。 最後,畫素經過一系列幀緩衝區操作,其中,它經過“深度緩衝區檢驗”(檢查一個片段是否遮擋另一個)、“混合”(用透明度混合兩個片段)以及其他操作,其當前的顏色與幀緩衝區中該位置已有的顏色結合。
- 幀緩衝區。 這些變化最終體現在最後的幀緩衝區上,通常顯示在螢幕上。
PS:該部分參考Mahesh Venkitachalam大神編寫的《Python極客專案程式設計》,程式碼可以檢視:https://github.com/electronut/pp
3.OpenGL繪製時鐘
最後補充“xiaoge2016老師”的一段趣味程式碼,透過OpenGL繪製時鐘,注意它是跳動的。
# -*- coding: utf-8 -*- from OpenGL.GL import * from OpenGL.GLU import * from OpenGL.GLUT import * import math import time h = 0 m = 0 s = 0 # 繪製影像函式 def Draw(): PI = 3.1415926 R = 0.5 TR = R - 0.05 glClear(GL_COLOR_BUFFER_BIT) glLineWidth(5) glBegin(GL_LINE_LOOP) for i in range(100): glVertex2f(R * math.cos(2 * PI / 100 * i), R * math.sin(2 * PI / 100 * i)) glEnd() glLineWidth(2) for i in range(100): glBegin(GL_LINES) glVertex2f(TR * math.sin(2 * PI / 12 * i), TR * math.cos(2 * PI / 12 * i)) glVertex2f(R * math.sin(2 * PI / 12 * i), R * math.cos(2 * PI / 12 * i)) glEnd() glLineWidth(1) h_Length = 0.2 m_Length = 0.3 s_Length = 0.4 count = 60.0 s_Angle = s / count count *= 60 m_Angle = (m * 60 + s) / count count *= 12 h_Angle = (h * 60 * 60 + m * 60 + s) / count glLineWidth(1) glBegin(GL_LINES) glVertex2f(0.0, 0.0) glVertex2f(s_Length * math.sin(2 * PI * s_Angle), s_Length * math.cos(2 * PI * s_Angle)) glEnd() glLineWidth(5) glBegin(GL_LINES) glVertex2f(0.0, 0.0) glVertex2f(h_Length * math.sin(2 * PI * h_Angle), h_Length * math.cos(2 * PI * h_Angle)) glEnd() glLineWidth(3) glBegin(GL_LINES) glVertex2f(0.0, 0.0) glVertex2f(m_Length * math.sin(2 * PI * m_Angle), m_Length * math.cos(2 * PI * m_Angle)) glEnd() glLineWidth(1) glBegin(GL_POLYGON) for i in range(100): glVertex2f(0.03 * math.cos(2 * PI / 100 * i), 0.03 * math.sin(2 * PI / 100 * i)); glEnd() glFlush() # 更新時間函式 def Update(): global h, m, s t = time.localtime(time.time()) h = int(time.strftime('%H', t)) m = int(time.strftime('%M', t)) s = int(time.strftime('%S', t)) glutPostRedisplay() # 主函式 if __name__ == "__main__": glutInit() glutInitDisplayMode(GLUT_SINGLE | GLUT_RGBA) glutInitWindowSize(400, 400) glutCreateWindow("My clock") glutDisplayFunc(Draw) glutIdleFunc(Update) glutMainLoop()
其執行結果如下圖所示:
四.總結
本篇文章主要講解Python和OpenGL基礎知識,包括安裝、基礎語法、繪製圖形等。希望對讀者有一定幫助,也希望這些知識點為讀者從事Python影像處理相關專案實踐或科學研究提供一定基礎。
參考文獻:
[1] 《OpenGL程式設計指南(第8版)》 作者:Dave Shreiner Granham Sellers等,王銳 譯
[2] 《Python極客專案程式設計》 作者:Mahesh Venkitachalam,王海鵬 譯
[3] 《OpenGL程式設計精粹》楊柏林 陳根浪 徐靜 編著
[4] 寫給 python 程式設計師的 OpenGL 教程 - 許老師(天元浪子)
[5] Python之OpenGL筆記(2):現代OpenGL程式設計常用的幾個通用函式 - 大龍老師
[6] python3+OpenGL環境配置 - GraceSkyer老師
[7] VS2012下基於Glut OpenGL顯示一些立體圖形示例程式 - yearafteryear老師
[8] Python——OpenGL - 白季飛龍老師
[9] python+opengl顯示三維模型小程式 - xiaoge2016
[10] https://github.com/electronut/pp