萬字長文詳解如何用 Python 玩轉 OpenGL | CSDN 博文精選

萬字長文詳解如何用 Python 玩轉 OpenGL | CSDN 博文精選萬字長文詳解如何用 Python 玩轉 OpenGL | CSDN 博文精選

作者 | 天元浪子

責編 | 伍杏玲

出品 | CSDN 博客

【CSDN 編者按】OpenGL(開放式圖形庫),用於渲染 2D、3D 矢量圖形的跨語言、跨平臺的應用程序編程接口,C、C++、Python、Java等語言都能支持 OpenGL。本文作者以 Python 語法為例,用兩萬字詳解 OpenGL 的理論知識、用法與實際操作,乾貨滿滿,一起來看看吧。

萬字長文詳解如何用 Python 玩轉 OpenGL | CSDN 博文精選

預備知識

OpenGL 是 Open Graphics Library 的簡寫,意為“開放式圖形庫”,是用於渲染 2D、3D 矢量圖形的跨語言、跨平臺的應用程序編程接口(API)。OpenGL 不是一個獨立的平臺,因此,它需要藉助於一種編程語言才能被使用。C / C++ / Python / Java 都可以很好支持 OpengGL,我當然習慣性選擇 Python 語言。

如果讀者是 Python 程序員,並且瞭解 NumPy,接下來的閱讀應該不會有任何障礙;否則,我建議先花半小時學習一下 Python 語言。關於 Python ,可以參考我的另一篇博文《數學建模三劍客MSN》。事實上,我覺得 Python 語言近乎於自然語言,只要讀者是程序員,即便不熟悉 Python ,讀起來也不會有多大問題。

另外,讀者也不必擔心數學問題。使用 OpenGL 不需要具備多麼高深的數學水平,只要能輔導初中學生的數學作業,就足夠用了。

一、座標系

在 OpenGL 的世界裡,有各式各樣的座標系。隨著對 OpenGL 概念的理解,我們至少會接觸到六種座標系,而初始只需要瞭解其中的三個就足夠用了(第一次閱讀這段話的時候,只需要瞭解世界座標系就可以了)。

  • 世界座標系(World Coordinates)

世界座標系是右手座標系,以屏幕中心為原點(0, 0, 0),且是始終不變的。

萬字長文詳解如何用 Python 玩轉 OpenGL | CSDN 博文精選
  • 視點座標系(Eye or Camera Coordinates)

視點座標是以視點為原點,以視線的方向為Z+軸正方向的座標系。OpenGL 管道會將世界座標先變換到視點座標,然後進行裁剪,只有在視線範圍(視景體)之內的場景才會進入下一階段的計算。

  • 屏幕座標系(Window or Screen Coordinates)

OpenGL 的重要功能之一就是將三維的世界座標經過變換、投影等計算,最終算出它在顯示設備上對應的位置,這個位置就稱為設備座標。在屏幕、打印機等設備上的座標是二維座標。值得一提的是,OpenGL 可以只使用設備的一部分進行繪製,這個部分稱為視區或視口(viewport)。投影得到的是視區內的座標(投影座標),從投影座標到設備座標的計算過程就是設備變換了。

二、投影

三維場景中的物體最終都會顯示在類似屏幕這樣的二維觀察平面上。將三維物體變為二維圖形的變換成為投影變換。最常用的投影有兩種:平行投影和透視投影。如下圖所示,F 是投影面,p1p2 為三維空間中的一條直線,p’1 和 p’2 分別是 p1 和 p2 在 F 上的投影,虛線表示投影線,O 為投影中心。

萬字長文詳解如何用 Python 玩轉 OpenGL | CSDN 博文精選
  • 平行投影

這裡所說的平行投影,特指正交平行投影——投影線垂直於投影面。將一個三維點 (x,y,z) 正交平行投影到 xoy 平面上,則投影點座標為 (x,y,0)。由於平行投影丟棄了深度信息,所以無法產生真實感,但可以保持物體之間相對大小關係不變。

  • 透視投影

透視投影將投影面置於觀察點和投影對象之間,距離觀察者越遠的物體,投影尺寸越小,投影效果具有真實感,常用於遊戲和仿真領域。

三、視景體

無論是平行投影還是透視投影,投影成像都是在投影面上——我們可以把投影面理解成顯示屏幕。世界座標系描述的三維空間是無限的,投影平面是無限的,但(我們能夠看到的)屏幕面積總是有限的,因此在投影變換時,通常只處理能夠顯示在屏幕上的那一部分三維空間。從無限三維空間中裁切出來的可以顯示在屏幕上的部分三維空間,我們稱之為視景體。視景體有六個面,分別是左右上下和前後面。

對於平行投影而言,視景體是一個矩形平行六面體;對於透視投影來說,視景體是一個稜臺。理解這一點並不難:因為越遠處的物體在投影窗口的透視投影越小,也就意味著填滿投影窗口需要更大的體量,視景體自然就變成了稜臺。

萬字長文詳解如何用 Python 玩轉 OpenGL | CSDN 博文精選

四、視口

對於平行投影而言,視口就是由視景體的左右上下四個面圍成的矩形,對於透視投影來說,視口就是視景體的前截面在投影窗口上的透視投影。

視口是 OpenGL 中比較重要的概念,現階段可以簡單理解成屏幕(或其他輸出設備)。事實上,視口和屏幕是相關但又不相同的,屏幕有固定的寬高比,而視口大小可以由用戶自行定義。通常,為了適應不同寬高比的屏幕,在設置視口時,會根據屏幕寬高比調整視景體(增加寬度或高度)。

五、視點

現實生活中,人們看到的三維空間物體的樣子取決於觀察者站在什麼角度去看。這裡麵包含著三個概念:

  • 觀察者的位置:眼睛在哪兒?

  • 觀察者的姿勢:站立還是倒立?左側臥還是右側臥?

  • 觀察對象:眼睛盯著哪裡?

對應在 OpenGL 中,也有同樣的概念,即視點的位置、瞄準方向的參考點,以及(向上的)方向。

六、OpenGL 變換

下圖是三維圖形的顯示流程。世界座標系中的三維物體經過視點變換和一系列幾何變換(平移、旋轉、縮放)之後,座標系變換為視點座標系;經過投影和裁剪之後,座標系變換為歸一化設備座標系;最後經過視口變換顯示在屏幕上,相應地,座標系變成了窗口座標系。

萬字長文詳解如何用 Python 玩轉 OpenGL | CSDN 博文精選
  • 視點變換:相當於設置視點的位置和方向

  • 模型變換:包括平移、旋轉、縮放等三種類型

  • 裁剪變換:根據視景體定義的六個面(和附加裁剪面)對三維空間裁剪

  • 視口變換:將視景體內投影的物體顯示在二維的視口平面上

萬字長文詳解如何用 Python 玩轉 OpenGL | CSDN 博文精選

安裝 PyOpenGL

如果想當然地使用 pip 如下所示安裝,可能會有一些麻煩。

pip install pyopengl

當我這樣安裝之後,運行 OpenGL 代碼,得到了這樣的錯誤信息:

FunctionError: Attempt to call an undefined function glutInit, check for bool(glutInit) before calling

原來,pip 默認安裝的是32位版本的PyOpenGL,而我的操作系統是64位的。建議點擊這裡下載適合自己的版本,直接安裝.whl文件。我是這樣安裝的:

pip install PyOpenGL-3.1.3b2-cp37-cp37m-win_amd64.whl
萬字長文詳解如何用 Python 玩轉 OpenGL | CSDN 博文精選

OpenGL 庫及函數簡介

我第一次接觸 OpenGL 的 GL / GLU / GLUT 的時候,一下就被這些長得像孿生兄弟的庫名字給整懵圈了,要不是內心強大,也許就跟 OpenGL 說再見了。時間久了才發現,OpenGL 的庫及函數命名規則非常合理,便於查找、記憶:

OpenGL函數的命名格式如下:

<庫前綴><根命令><可選的參數個數><可選的參數類型>

常見的庫前綴有 gl、glu、glut、aux、wgl、glx、agl 等。庫前綴表示該函數屬於 OpenGL 哪一個開發庫。從函數名後面中還可以看出需要多少個參數以及參數的類型。I 代表 int 型,f 代表 float 型,d 代表 double 型,u 代表無符號整型。例如 glColor3f 表示了該函數屬於gl庫,參數是三個浮點數。

OpenGL 函數庫相關的 API 有核心庫(gl)、實用庫(glu)、實用工具庫(glut)、輔助庫(aux)、窗口庫(glx、agl、wgl)和擴展函數庫等。gl是核心,glu是對gl的部分封裝。glut是為跨平臺的OpenGL程序的工具包,比aux功能強大。glx、agl、wgl 是針對不同窗口系統的函數。擴展函數庫是硬件廠商為實現硬件更新利用OpenGL的擴展機制開發的函數。本文僅對常用的四個庫做簡單介紹。

一、OpenGL 核心庫 GL

核心庫包含有115個函數,函數名的前綴為gl。這部分函數用於常規的、核心的圖形處理。此函數由gl.dll來負責解釋執行。由於許多函數可以接收不同數以下幾類。據類型的參數,因此派生出來的函數原形多達300多個。核心庫中的函數主要可以分為以下幾類函數:

  • 繪製基本幾何圖元的函數:

    glBegain、glEnd、glNormal*、glVertex*

  • 矩陣操作、幾何變換和投影變換的函數:

    如矩陣入棧函數glPushMatrix,矩陣出棧函數glPopMatrix,裝載矩陣函數glLoadMatrix,矩陣相乘函數glMultMatrix,當前矩陣函數glMatrixMode和矩陣標準化函數glLoadIdentity,幾何變換函數glTranslate*、glRotate*和glScale*,投影變換函數glOrtho、glFrustum和視口變換函數glViewport

  • 顏色、光照和材質的函數:

    如設置顏色模式函數glColor*、glIndex*,設置光照效果的函數glLight* 、glLightModel*和設置材質效果函數glMaterial

  • 顯示列表函數:

    主要有創建、結束、生成、刪除和調用顯示列表的函數glNewList、glEndList、glGenLists、glCallList和glDeleteLists

  • 紋理映射函數:

    主要有一維紋理函數glTexImage1D、二維紋理函數glTexImage2D、設置紋理參數、紋理環境和紋理座標的函數glTexParameter*、glTexEnv*和glTetCoord*

  • 特殊效果函數:

    融合函數glBlendFunc、反走樣函數glHint和霧化效果glFog*

  • 光柵化、象素操作函數:

    如象素位置glRasterPos*、線型寬度glLineWidth、多邊形繪製模式glPolygonMode,讀取象素glReadPixel、複製象素glCopyPixel

  • 選擇與反饋函數:

    主要有渲染模式glRenderMode、選擇緩衝區glSelectBuffer和反饋緩衝區glFeedbackBuffer

  • 曲線與曲面的繪製函數:

    生成曲線或曲面的函數glMap*、glMapGrid*,求值器的函數glEvalCoord* glEvalMesh*

  • 狀態設置與查詢函數:

    glGet*、glEnable、glGetError

二、OpenGL 實用庫 GLU

包含有43個函數,函數名的前綴為glu。OpenGL提供了強大的但是為數不多的繪圖命令,所有較複雜的繪圖都必須從點、線、面開始。Glu 為了減輕繁重的編程工作,封裝了OpenGL函數,Glu函數通過調用核心庫的函數,為開發者提供相對簡單的用法,實現一些較為複雜的操作。此函數由glu.dll來負責解釋執行。OpenGL中的核心庫和實用庫可以在所有的OpenGL平臺上運行。主要包括了以下幾種:

  • 輔助紋理貼圖函數:

    gluScaleImage 、gluBuild1Dmipmaps、gluBuild2Dmipmaps

  • 座標轉換和投影變換函數:

    定義投影方式函數gluPerspective、gluOrtho2D 、gluLookAt,拾取投影視景體函數gluPickMatrix,投影矩陣計算gluProject和gluUnProject

  • 多邊形鑲嵌工具:

    gluNewTess、gluDeleteTess、gluTessCallback、gluBeginPolygon、gluTessVertex、gluNextContour、gluEndPolygon

  • 二次曲面繪製工具:

    主要有繪製球面、錐面、柱面、圓環面gluNewQuadric、gluSphere、gluCylinder、gluDisk、gluPartialDisk、gluDeleteQuadric

  • 非均勻有理B樣條繪製工具:

    主要用來定義和繪製Nurbs曲線和曲面,包括gluNewNurbsRenderer、gluNurbsCurve、gluBeginSurface、gluEndSurface、gluBeginCurve、gluNurbsProperty

  • 錯誤反饋工具:

    獲取出錯信息的字符串gluErrorString

三、OpenGL 工具庫 GLUT

包含大約30多個函數,函數名前綴為glut。glut是不依賴於窗口平臺的OpenGL工具包,由Mark KLilgrad在SGI編寫(現在在Nvidia),目的是隱藏不同窗口平臺API的複雜度。函數以glut開頭,它們作為aux庫功能更強的替代品,提供更為複雜的繪製功能,此函數由glut.dll來負責解釋執行。

由於glut中的窗口管理函數是不依賴於運行環境的,因此OpenGL中的工具庫可以在X-Window、Windows NT、OS/2等系統下運行,特別適合於開發不需要複雜界面的OpenGL示例程序。對於有經驗的程序員來說,一般先用glut理順3D圖形代碼,然後再集成為完整的應用程序。這部分函數主要包括:

  • 窗口操作函數:

    窗口初始化、窗口大小、窗口位置函數等 glutInit、glutInitDisplayMode、glutInitWindowSize、glutInitWindowPosition

  • 回調函數:

    響應刷新消息、鍵盤消息、鼠標消息、定時器函數 GlutDisplayFunc、glutPostRedisplay、glutReshapeFunc、glutTimerFunc、glutKeyboardFunc、glutMouseFunc

  • 創建複雜的三維物體:

    這些和aux庫的函數功能相同。

  • 菜單函數:

    創建添加菜單的函數 GlutCreateMenu、glutSetMenu、glutAddMenuEntry、glutAddSubMenu 和 glutAttachMenu

  • 程序運行函數:

    glutMainLoop

四、Windows 專用庫 WGL

針對Windows平臺的擴展,包含有16個函數,函數名前綴為wgl。這部分函數主要用於連接OpenGL和Windows ,以彌補OpenGL在文本方面的不足。Windows專用庫只能用於Windows環境中。這類函數主要包括以下幾類:

  • 繪圖上下文相關函數:

    wglCreateContext、wglDeleteContext、wglGetCurrentContent、wglGetCurrentDC、wglDeleteContent

  • 文字和文本處理函數:

    wglUseFontBitmaps、wglUseFontOutlines

  • 覆蓋層、地層和主平面層處理函數:

    wglCopyContext、wglCreateLayerPlane、wglDescribeLayerPlane、wglReakizeLayerPlatte

  • 其他函數:

    wglShareLists、wglGetProcAddress

萬字長文詳解如何用 Python 玩轉 OpenGL | CSDN 博文精選

開始 OpenGL 的奇幻之旅

一、OpenGL 基本圖形的繪製

  • 設置顏色

設置顏色的函數有幾十個,都是以 glColor 開頭,後面跟著參數個數和參數類型。參數可以是 0 到 255 之間的無符號整數,也可以是 0 到 1 之間的浮點數。三個參數分別表示 RGB 分量,第四個參數表示透明度(其實叫不透明度更恰當)。以下最常用的兩個設置顏色的方法:

glColor3f(1.0,0.0,0.0) # 設置當前顏色為紅色

glColor4f(0.0,1.0,1.0,1.0) # 設置當前顏色為青色,不透明度

glColor3ub(0, 0, 255) # 設置當前顏色為藍色

glColor 也支持將三個或四個參數以向量方式傳遞,例如:

glColor3fv([0.0,1.0,0.0]) # 設置當前顏色為綠色

特別提示:OpenGL 是使用狀態機模式,顏色是一個狀態變量,設置顏色就是改變這個狀態變量並一直生效,直到再次調用設置顏色的函數。除了顏色,OpenGL 還有很多的狀態變量或模式。在任何時間,都可以查詢每個狀態變量的當前值,還可以用 glPushAttrib 或 glPushClientAttrib 把狀態變量的集合保存起來,必要的時候,再用 glPopAttrib 或 glPopClientAttrib 恢復狀態變量。

  • 設置頂點

頂點(vertex)是 OpengGL 中非常重要的概念,描述線段、多邊形都離不開頂點。和設置顏色類似,設置頂點的函數也有幾十個,都是以 glVertex 開頭,後面跟著參數個數和參數類型,同樣也支持將多個以向量方式傳遞。兩個參數的話,分別表示 xy 座標,三個參數則分別表示 xyz 座標。如有第四個參數,則表示該點的齊次座標 w;否則,默認 w=1。至於什麼是齊次座標,顯然超出了初中數學的範疇,在此不做探討。

glVertex2f(1.0,0.5) # xoy平面上的點,z=0

glVertex3f(0.5,1.0,0.0) # 三維空間中的點

  • 繪製基本圖形

僅僅設置顏色和頂點,並不能畫出來什麼。我們可以在任何時候改變顏色,但所有的頂點設置,都必須包含在 glBegin 和 glEnd 之間,而 glBegin 的參數則指定了將這些頂點畫成什麼。以下是 glBegin 可能的參數選項:

萬字長文詳解如何用 Python 玩轉 OpenGL | CSDN 博文精選

二、第一個 OpenGL 程序

通常,我們使用工具庫(GLUT)創建 OpenGL 應用程序。為啥不用 GL 或者 GLU 庫呢?畫畫之前總得先有一塊畫布吧,不能直接拿起畫筆就開畫。前文說過,工具庫主要提供窗口相關的函數,有了窗口,就相當於有了畫布,而核心庫和實用庫,就好比各式各樣的畫筆、顏料。使用工具庫(GLUT)創建 OpenGL 應用程序只需要四步(當然,前提是你需要先準備好繪圖函數,並給它取一個合適的名字):

  • 初始化glut庫

  • 創建glut窗口

  • 註冊繪圖的回調函數

  • 進入glut主循環

OK,鋪墊了這麼多之後,我們終於開始第一個 OpenGL 應用程序了:繪製三維空間的世界座標系,在座標原點的後方(z軸的負半區)畫一個三角形。代碼如下:

# -*- coding: utf-8 -*-

# -------------------------------------------

# quidam_01.py 三維空間的世界座標系和三角形

# -------------------------------------------

from OpenGL.GL import *

from OpenGL.GLUT import *

def draw:

# ---------------------------------------------------------------

glBegin(GL_LINES) # 開始繪製線段(世界座標系)

# 以紅色繪製x軸

glColor4f(1.0, 0.0, 0.0, 1.0) # 設置當前顏色為紅色不透明

glVertex3f(-0.8, 0.0, 0.0) # 設置x軸頂點(x軸負方向)

glVertex3f(0.8, 0.0, 0.0) # 設置x軸頂點(x軸正方向)

# 以綠色繪製y軸

glColor4f(0.0, 1.0, 0.0, 1.0) # 設置當前顏色為綠色不透明

glVertex3f(0.0, -0.8, 0.0) # 設置y軸頂點(y軸負方向)

glVertex3f(0.0, 0.8, 0.0) # 設置y軸頂點(y軸正方向)

# 以藍色繪製z軸

glColor4f(0.0, 0.0, 1.0, 1.0) # 設置當前顏色為藍色不透明

glVertex3f(0.0, 0.0, -0.8) # 設置z軸頂點(z軸負方向)

glVertex3f(0.0, 0.0, 0.8) # 設置z軸頂點(z軸正方向)

glEnd # 結束繪製線段

# ---------------------------------------------------------------

glBegin(GL_TRIANGLES) # 開始繪製三角形(z軸負半區)

glColor4f(1.0, 0.0, 0.0, 1.0) # 設置當前顏色為紅色不透明

glVertex3f(-0.5, -0.366, -0.5) # 設置三角形頂點

glColor4f(0.0, 1.0, 0.0, 1.0) # 設置當前顏色為綠色不透明

glVertex3f(0.5, -0.366, -0.5) # 設置三角形頂點

glColor4f(0.0, 0.0, 1.0, 1.0) # 設置當前顏色為藍色不透明

glVertex3f(0.0, 0.5, -0.5) # 設置三角形頂點

glEnd

# 結束繪製三角形

# ---------------------------------------------------------------

glFlush # 清空緩衝區,將指令送往硬件立即執行

if __name__ == "__main__":

glutInit # 1. 初始化glut庫

glutCreateWindow('Quidam Of OpenGL') # 2. 創建glut窗口

glutDisplayFunc(draw) # 3. 註冊回調函數draw

glutMainLoop # 4. 進入glut主循環

運行代碼,我這裡顯示結果如下面左圖所示。如果嘗試運行這段代碼出錯的話,我猜應該是 PyOpenGL 安裝出現了問題,建議返回到前面重讀 PyOpenGL 的安裝。

萬字長文詳解如何用 Python 玩轉 OpenGL | CSDN 博文精選

短暫的激動之後,你可能會嘗試畫一些其他的線段,變換顏色或者透明度,甚至繪製多邊形。很快你會發現,我們的第一個程序有很多問題,比如:

  1. 窗口的標題不能使用中文,否則會顯示亂碼

  2. 窗口的初始大小和位置無法改變

  3. 改變窗口的寬高比,三角形寬高比也會改變(如上面右圖所示)

  4. 三角形不應該遮擋座標軸

  5. 改變顏色的透明度無效

  6. 不能縮放旋轉

沒關係,除了第1個問題我不知道怎麼解決(貌似無解),其他問題都不是事兒。和我們的代碼相比,一個真正實用的 OpenGL 程序,還有許多工作要做:

  • 設置初始顯示模式

  • 初始化畫布

  • 繪圖函數裡面需要增加:清除屏幕及深度緩存、投影設置、模型試圖設置

  • 綁定鼠標鍵盤的事件函數

三、設置初始顯示模式

初始化 glut 庫的時候,我們一般都要用 glutInitDisplayMode 來設置初始的顯示模式,它的參數可以是下表中參數的組合。

萬字長文詳解如何用 Python 玩轉 OpenGL | CSDN 博文精選

使用雙緩存窗口,可以避免重繪時產生抖動的感覺。我一般選擇 GLUT_DOUBLE | GLUT_ALPHA | GLUT_DEPTH 作為參數來設置初始的顯示模式。

四、初始化畫布

開始繪圖之前,需要對畫布做一些初始化工作,這些工作只需要做一次。比如:

glClearColor(0.0, 0.0, 0.0, 1.0) # 設置畫布背景色。注意:這裡必須是4個參數

glEnable(GL_DEPTH_TEST) # 開啟深度測試,實現遮擋關係

glDepthFunc(GL_LEQUAL) # 設置深度測試函數(GL_LEQUAL只是選項之一)

如有必要,還可以開啟失真校正(反走樣)、開啟表面剔除等。

五、清除屏幕及深度緩存

每次重繪之前,需要先清除屏幕及深度緩存。這項操作一般放在繪圖函數的開頭。

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

六、設置投影

投影設置也是每次重繪都需要的步驟之一。glOrtho 用來設置平行投影,glFrustum 用來設置透視投影。這兩個函數的參數相同,都是視景體的 left / right / bottom / top / near / far 六個面。

視景體的 left / right / bottom / top 四個面圍成的矩形,就是視口。near 就是投影面,其值是投影面距離視點的距離,far 是視景體的後截面,其值是後截面距離視點的距離。far 和 near 的差值,就是視景體的深度。視點和視景體的相對位置關係是固定的,視點移動時,視景體也隨之移動。

我個人認為,視景體是 OpengGL 最重要、最核心的概念,它和視口、視點、投影面、縮放、漫遊等概念密切關聯。只有正確理解了視景體,才能正確設置它的六個參數,才能呈現出我們期望的效果。

為了在窗口寬高比改變時,繪製的對象仍然保持固定的寬高比,一般在做投影變換時,需要根據窗口的寬高比適當調整視景體的 left / right 或者 bottom / top 參數。

假設 view 是視景體,width 和 height 是窗口的寬度和高度,在投影變換之前,需要先聲明是對投影矩陣的操作,並將投影矩陣單位化:

glMatrixMode(GL_PROJECTION)

glLoadIdentity

if width > height:

k = width / height

glFrustum(view [0]*k, view [1]*k, view [2], view [3], view [4], view [5])

else:

k = height / width

glFrustum(view [0], view [1], view [2]*k, view [3]*k, view [4], view [5])

七、設置視點

視點是和視景體關聯的概念。設置視點需要考慮眼睛在哪兒、看哪兒、頭頂朝哪兒,分別對應著eye、lookat 和 eye_up 三個向量。

gluLookAt(

eye[0], eye[1], eye[2],

look_at[0], look_at[1], look_at[2],

eye_up[0], eye_up[1], eye_up[2]

)

八、設置視口

視口也是和視景體關聯的概念,相對簡單一點。

glViewport(0, 0, width, height)

九、設置模型視圖

模型平移、旋轉、縮放等幾何變換,需要切換到模型矩陣:

glMatrixMode(GL_MODELVIEW)

glLoadIdentity

glScale(1.0, 1.0, 1.0)

十、捕捉鼠標事件、鍵盤事件和窗口事件

GLUT 庫提供了幾個函數幫我們捕捉鼠標事件、鍵盤事件和窗口事件:

  • glutMouseFunc

    該函數捕捉鼠標點擊和滾輪操作,返回4個參數給被綁定的事件函數:鍵(左鍵/右鍵/中鍵/滾輪上/滾輪下)、狀態(1/0)、x座標、y座標

  • glutMotionFunc

    該函數捕捉有一個鼠標鍵被按下時的鼠標移動給被綁定的事件函數,返回2個參數:x座標、y座標

  • glutPassiveMotionFunc

    該函數捕捉鼠標移動,返回2個參數給被綁定的事件函數:x座標、y座標

  • glutEntryFunc

    該函數捕捉鼠標離開或進入窗口區域,返回1個參數給被綁定的事件函數:GLUT_LEFT 或者 GLUT_ENTERED

    glutKeyboardFunc(keydown)

    該函數捕捉鍵盤按鍵被按下,返回3個參數給被綁定的事件函數:被按下的鍵,x座標、y座標

  • glutReshapeFunc

    該函數捕捉窗口被改變大小,返回2個參數給被綁定的事件函數:窗口寬度、窗口高度

如果我們需要捕捉這些事件,只需要定義事件函數,註冊相應的函數就行:

def reshape(width, height):

pass

def mouseclick(button, state, x, y):

pass

def mousemotion(x, y):

pass

def keydown(key, x, y):

pass

glutReshapeFunc(reshape) # 註冊響應窗口改變的函數reshape

glutMouseFunc(mouseclick) # 註冊響應鼠標點擊的函數mouseclick

glutMotionFunc(mousemotion) # 註冊響應鼠標拖拽的函數mousemotion

glutKeyboardFunc(keydown) # 註冊鍵盤輸入的函數keydown

十一、綜合應用

是時候把我們上面講的這些東西完整的演示一下了。下面的代碼還是畫了世界座標系,並在原點前後各畫了一個三角形。鼠標可以拖拽視點繞參考點旋轉(二者距離保持不變),滾輪可以縮放模型。

敲擊退格鍵或回車鍵可以讓視點遠離或接近參考點。敲擊 x/y/z 可以減小參考點對應的座標值,敲擊 X/Y/Z 可以增大參考點對應的座標值。敲擊空格鍵可以切換投影模式。

萬字長文詳解如何用 Python 玩轉 OpenGL | CSDN 博文精選

上圖左是平行投影模式的顯示效果,上圖右是透視投影模式的顯示效果。代碼如下:

# -*- coding: utf-8 -*-

# -------------------------------------------

# quidam_02.py 旋轉、縮放、改變視點和參考點

# -------------------------------------------

from OpenGL.GL import *

from OpenGL.GLU import *

from OpenGL.GLUT import *

import numpy as np

IS_PERSPECTIVE = True # 透視投影

VIEW = np.array([-0.8, 0.8, -0.8, 0.8, 1.0, 20.0]) # 視景體的left/right/bottom/top/near/far六個面

SCALE_K = np.array([1.0, 1.0, 1.0]) # 模型縮放比例

EYE = np.array([0.0, 0.0, 2.0]) # 眼睛的位置(默認z軸的正方向)

LOOK_AT = np.array([0.0, 0.0, 0.0]) # 瞄準方向的參考點(默認在座標原點)

EYE_UP = np.array([0.0, 1.0, 0.0]) # 定義對觀察者而言的上方(默認y軸的正方向)

WIN_W, WIN_H = 640, 480 # 保存窗口寬度和高度的變量

LEFT_IS_DOWNED = False # 鼠標左鍵被按下

MOUSE_X, MOUSE_Y = 0, 0 # 考察鼠標位移量時保存的起始位置

def getposture:

global EYE, LOOK_AT

dist = np.sqrt(np.power((EYE-LOOK_AT), 2).sum)

if dist > 0:

phi = np.arcsin((EYE[1]-LOOK_AT[1])/dist)

theta = np.arcsin((EYE[0]-LOOK_AT[0])/(dist*np.cos(phi)))

else:

phi = 0.0

theta = 0.0

return dist, phi, theta

DIST, PHI, THETA = getposture # 眼睛與觀察目標之間的距離、仰角、方位角

def init:

glClearColor(0.0, 0.0, 0.0, 1.0) # 設置畫布背景色。注意:這裡必須是4個參數

glEnable(GL_DEPTH_TEST) # 開啟深度測試,實現遮擋關係

glDepthFunc(GL_LEQUAL) # 設置深度測試函數(GL_LEQUAL只是選項之一)

def draw:

global IS_PERSPECTIVE, VIEW

global EYE, LOOK_AT, EYE_UP

global SCALE_K

global WIN_W, WIN_H

# 清除屏幕及深度緩存

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

# 設置投影(透視投影)

glMatrixMode(GL_PROJECTION)

glLoadIdentity

if WIN_W > WIN_H:

if IS_PERSPECTIVE:

glFrustum(VIEW[0]*WIN_W/WIN_H, VIEW[1]*WIN_W/WIN_H, VIEW[2], VIEW[3], VIEW[4], VIEW[5])

else:

glOrtho(VIEW[0]*WIN_W/WIN_H, VIEW[1]*WIN_W/WIN_H, VIEW[2], VIEW[3], VIEW[4], VIEW[5])

else:

if IS_PERSPECTIVE:

glFrustum(VIEW[0], VIEW[1], VIEW[2]*WIN_H/WIN_W, VIEW[3]*WIN_H/WIN_W, VIEW[4], VIEW[5])

else:

glOrtho(VIEW[0], VIEW[1], VIEW[2]*WIN_H/WIN_W, VIEW[3]*WIN_H/WIN_W, VIEW[4], VIEW[5])

# 設置模型視圖

glMatrixMode(GL_MODELVIEW)

glLoadIdentity

# 幾何變換

glScale(SCALE_K[0], SCALE_K[1], SCALE_K[2])

# 設置視點

gluLookAt(

EYE[0], EYE[1], EYE[2],

LOOK_AT[0], LOOK_AT[1], LOOK_AT[2],

EYE_UP[0], EYE_UP[1], EYE_UP[2]

)

# 設置視口

glViewport(0, 0, WIN_W, WIN_H)

# ---------------------------------------------------------------

glBegin(GL_LINES) # 開始繪製線段(世界座標系)

# 以紅色繪製x軸

glColor4f(1.0, 0.0, 0.0, 1.0) # 設置當前顏色為紅色不透明

glVertex3f(-0.8, 0.0, 0.0) # 設置x軸頂點(x軸負方向)

glVertex3f(0.8, 0.0, 0.0) # 設置x軸頂點(x軸正方向)

# 以綠色繪製y軸

glColor4f(0.0, 1.0, 0.0, 1.0) # 設置當前顏色為綠色不透明

glVertex3f(0.0, -0.8, 0.0) # 設置y軸頂點(y軸負方向)

glVertex3f(0.0, 0.8, 0.0) # 設置y軸頂點(y軸正方向)

# 以藍色繪製z軸

glColor4f(0.0, 0.0, 1.0, 1.0) # 設置當前顏色為藍色不透明

glVertex3f(0.0, 0.0, -0.8) # 設置z軸頂點(z軸負方向)

glVertex3f(0.0, 0.0, 0.8) # 設置z軸頂點(z軸正方向)

glEnd # 結束繪製線段

# ---------------------------------------------------------------

glBegin(GL_TRIANGLES) # 開始繪製三角形(z軸負半區)

glColor4f(1.0, 0.0, 0.0, 1.0) # 設置當前顏色為紅色不透明

glVertex3f(-0.5, -0.366, -0.5) # 設置三角形頂點

glColor4f(0.0, 1.0, 0.0, 1.0) # 設置當前顏色為綠色不透明

glVertex3f(0.5, -0.366, -0.5) # 設置三角形頂點

glColor4f(0.0, 0.0, 1.0, 1.0) # 設置當前顏色為藍色不透明

glVertex3f(0.0, 0.5, -0.5) # 設置三角形頂點

glEnd # 結束繪製三角形

# ---------------------------------------------------------------

glBegin(GL_TRIANGLES) # 開始繪製三角形(z軸正半區)

glColor4f(1.0, 0.0, 0.0, 1.0) # 設置當前顏色為紅色不透明

glVertex3f(-0.5, 0.5, 0.5) # 設置三角形頂點

glColor4f(0.0, 1.0, 0.0, 1.0) # 設置當前顏色為綠色不透明

glVertex3f(0.5, 0.5, 0.5) # 設置三角形頂點

glColor4f(0.0, 0.0, 1.0, 1.0) # 設置當前顏色為藍色不透明

glVertex3f(0.0, -0.366, 0.5) # 設置三角形頂點

glEnd # 結束繪製三角形

# ---------------------------------------------------------------

glutSwapBuffers # 切換緩衝區,以顯示繪製內容

def reshape(width, height):

global WIN_W, WIN_H

WIN_W, WIN_H = width, height

glutPostRedisplay

def mouseclick(button, state, x, y):

global SCALE_K

global LEFT_IS_DOWNED

global MOUSE_X, MOUSE_Y

MOUSE_X, MOUSE_Y = x, y

if button == GLUT_LEFT_BUTTON:

LEFT_IS_DOWNED = state==GLUT_DOWN

elif button == 3:

SCALE_K *= 1.05

glutPostRedisplay

elif button == 4:

SCALE_K *= 0.95

glutPostRedisplay

def mousemotion(x, y):

global LEFT_IS_DOWNED

global EYE, EYE_UP

global MOUSE_X, MOUSE_Y

global DIST, PHI, THETA

global WIN_W, WIN_H

if LEFT_IS_DOWNED:

dx = MOUSE_X - x

dy = y - MOUSE_Y

MOUSE_X, MOUSE_Y = x, y

PHI += 2*np.pi*dy/WIN_H

PHI %= 2*np.pi

THETA += 2*np.pi*dx/WIN_W

THETA %= 2*np.pi

r = DIST*np.cos(PHI)

EYE[1] = DIST*np.sin(PHI)

EYE[0] = r*np.sin(THETA)

EYE[2] = r*np.cos(THETA)

if 0.5*np.pi < PHI < 1.5*np.pi:

EYE_UP[1] = -1.0

else:

EYE_UP[1] = 1.0

glutPostRedisplay

def keydown(key, x, y):

global DIST, PHI, THETA

global EYE, LOOK_AT, EYE_UP

global IS_PERSPECTIVE, VIEW

if key in [b'x', b'X', b'y', b'Y', b'z', b'Z']:

if key == b'x': # 瞄準參考點 x 減小

LOOK_AT[0] -= 0.01

elif key == b'X': # 瞄準參考 x 增大

LOOK_AT[0] += 0.01

elif key == b'y': # 瞄準參考點 y 減小

LOOK_AT[1] -= 0.01

elif key == b'Y': # 瞄準參考點 y 增大

LOOK_AT[1] += 0.01

elif key == b'z': # 瞄準參考點 z 減小

LOOK_AT[2] -= 0.01

elif key == b'Z': # 瞄準參考點 z 增大

LOOK_AT[2] += 0.01

DIST, PHI, THETA = getposture

glutPostRedisplay

elif key == b'\r': # 回車鍵,視點前進

EYE = LOOK_AT + (EYE - LOOK_AT) * 0.9

DIST, PHI, THETA = getposture

glutPostRedisplay

elif key == b'\\x08': # 退格鍵,視點後退

EYE = LOOK_AT + (EYE - LOOK_AT) * 1.1

DIST, PHI, THETA = getposture

glutPostRedisplay

elif key == b' ': # 空格鍵,切換投影模式

IS_PERSPECTIVE = not IS_PERSPECTIVE

glutPostRedisplay

if __name__ == "__main__":

glutInit

displayMode = GLUT_DOUBLE | GLUT_ALPHA | GLUT_DEPTH

glutInitDisplayMode(displayMode)

glutInitWindowSize(WIN_W, WIN_H)

glutInitWindowPosition(300, 200)

glutCreateWindow('Quidam Of OpenGL')

init # 初始化畫布

glutDisplayFunc(draw) # 註冊回調函數draw

glutReshapeFunc(reshape) # 註冊響應窗口改變的函數reshape

glutMouseFunc(mouseclick) # 註冊響應鼠標點擊的函數mouseclick

glutMotionFunc(mousemotion) # 註冊響應鼠標拖拽的函數mousemotion

glutKeyboardFunc(keydown) # 註冊鍵盤輸入的函數keydown

glutMainLoop # 進入glut主循環

十二、小結

雖然還有很多領域需要我們繼續探索,比如燈光、材質、霧化、拾取等,但那不是奇幻之旅的目標。奇幻之旅僅僅是幫助讀者建立 OpenGL 的基本概念。至此,我們基本完成了任務。

萬字長文詳解如何用 Python 玩轉 OpenGL | CSDN 博文精選

加速渲染

實際應用 OpenGL 繪製三維圖像時,往往需要處理數以萬計的頂點,有時甚至是百萬級、千萬級。我們通常不會在繪製函數裡面傳送這些數據,而是在繪製之前,將這些數據提前傳送到GPU。繪製函數每次繪製時,只需要從GPU的緩存中取出數據即可,極大地提高了效率。這個機制地實現,依賴於頂點緩衝區對象(Vertex Buffer Object),簡稱VBO。

儘管 VBO 是顯卡的擴展,其實沒有用到GPU運算,也就是說 VBO 不用寫著色語言,直接用opengl函數就可以調用,主要目的是用於加快渲染的速。

VBO 將頂點信息放到 GPU 中,GPU 在渲染時去緩存中取數據,二者中間的橋樑是 GL-Context。GL-Context 整個程序一般只有一個,所以如果一個渲染流程裡有兩份不同的繪製代碼,GL-context 就負責在他們之間進行切換。這也是為什麼要在渲染過程中,在每份繪製代碼之中會有 glBindbuffer、glEnableVertexAttribArray、glVertexAttribPointer。如果把這些都放到初始化時候完成,使用一種結構記錄該次繪製所需要的所有 VBO 所需信息,把它保存到 VBO特定位置,繪製的時候直接在這個位置取信息繪製,會簡化渲染流程、提升渲染速度。這就是 VAO 概念產生的初衷。

VAO 的全名是 Vertex Array Object,首先,它不是 Buffer-Object,所以不用作存儲數據;其次,它針對“頂點”而言,也就是說它跟“頂點的繪製”息息相關。VAO 記錄的是一次繪製中所需要的信息,這包括“數據在哪裡 glBindBuffer”、“數據的格式是怎麼樣的 glVertexAttribPointer”、shader-attribute 的 location 的啟用 glEnableVertexAttribArray。

根據我查到的資料,幾乎所有的顯卡都支持 VBO,但不是所有的顯卡都支持 VAO,而 VAO 僅僅是優化了 VBO 的使用方法,對於加速並沒有實質性的影響,因此本文只討論 VBO 技術。

一、創建頂點緩衝區對象(VBO)

假定畫一個六面體,頂點是這樣的:

# -*- coding: utf-8 -*-

# 六面體數據

# ------------------------------------------------------

# v4----- v5

# /| /|

# v0------v1|

# | | | |

# | v7----|-v6

# |/ |/

# v3------v2

# 頂點集

vertices = np.array([

-0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, -0.5, 0.5, -0.5, -0.5, 0.5, # v0-v1-v2-v3

-0.5, 0.5, -0.5, 0.5, 0.5, -0.5, 0.5, -0.5, -0.5, -0.5, -0.5, -0.5 # v4-v5-v6-v7

], dtype=np.float32)

# 索引集

indices = np.array([

0, 1, 2, 3, # v0-v1-v2-v3 (front)

4, 5, 1, 0, # v4-v5-v1-v0 (top)

3, 2, 6, 7, # v3-v2-v6-v7 (bottom)

5, 4, 7, 6, # v5-v4-v7-v6 (back)

1, 5, 6, 2, # v1-v5-v6-v2 (right)

4, 0, 3, 7 # v4-v0-v3-v7 (left)

], dtype=np.int)

在GPU上創建VBO如下:

from OpenGL.arrays import vbo

vbo_vertices = vbo.VBO(vertices)

vbo_indices = vbo.VBO(indices, target=GL_ELEMENT_ARRAY_BUFFER)

創建 頂點 VBO 時,默認 target=GL_ARRAY_BUFFER, 而創建索引 VBO 時,target=GL_ELEMENT_ARRAY_BUFFER,因為頂點的數據類型是 np.float32,索引的數據類型是np.int。

在VBO保存的頂點數據集,除了頂點信息外,還可以包含顏色、法線、紋理等數據,這就是頂點混合數組的概念。假定我們在上面的頂點集中增加每個頂點的顏色,則可以寫成這樣:

vertices = np.array([

0.3, 0.6, 0.9, -0.35, 0.35, 0.35, # c0-v0

0.6, 0.9, 0.3, 0.35, 0.35, 0.35, # c1-v1

0.9, 0.3, 0.6, 0.35, -0.35, 0.35, # c2-v2

0.3, 0.9, 0.6, -0.35, -0.35, 0.35, # c3-v3

0.6, 0.3, 0.9, -0.35, 0.35, -0.35, # c4-v4

0.9, 0.6, 0.3, 0.35, 0.35, -0.35, # c5-v5

0.3, 0.9, 0.9, 0.35, -0.35, -0.35, # c6-v6

0.9, 0.9, 0.3, -0.35, -0.35, -0.35 # c7-v7

], dtype=np.float32)

二、分離頂點混合數組

使用 glInterleavedArrays 函數可以從頂點混合數組中分離頂點、顏色、法線和紋理。比如,對只包含頂點信息的頂點混合數組:

vbo_indices.bind

glInterleavedArrays(GL_V3F, 0, None)

如果頂點混合數組包含了顏色和頂點信息:

vbo_indices.bind

glInterleavedArrays(GL_C3F_V3F, 0, None)

glInterleavedArrays 函數第一個參數總共有14個選項,分別是:

  • GL_V2F

  • GL_V3F

  • GL_C4UB_V2F

  • GL_C4UB_V3F

  • GL_C3F_V3F

  • GL_N3F_V3F

  • GL_C4F_N3F_V3F

  • GL_T2F_V3F

  • GL_T4F_V4F

  • GL_T2F_C4UB_V3F

  • GL_T2F_C3F_V3F

  • GL_T2F_N3F_V3F

  • GL_T2F_C4F_N3F_V3F

  • GL_T4F_C4F_N3F_V4F

三、使用頂點緩衝區對象(VBO)

使用glDrawElements 等函數繪製前,需要先綁定頂點數據集和索引數據集,然後使用glInterleavedArrays 分理出頂點、顏色、法線等數據。

vbo_indices.bind

glInterleavedArrays(GL_V3F, 0, None)

vbo_indices.bind

glDrawElements(GL_QUADS, int(vbo_indices .size/4), GL_UNSIGNED_INT, None)

vbo_indices.unbind

vbo_indices.unbind

致謝:

寫作過程中,我參考了很多資料,包括紙質書籍和網頁,列寫於此,一併致謝!

  • 《OpenGL編程精粹》楊柏林 陳根浪 徐靜 編著

  • Opengl開發庫介紹

  • OpenGL的API函數使用手冊

  • glut處理鼠標事件

  • Learn OpenGL

原文:https://blog.csdn.net/xufive/article/details/86565130

聲明:本文系CSDN博客原創文章,轉載請聯繫原作者。

【End】

“只講技術,拒絕空談!”2019 AI開發者大會將於9月6日-7日在北京舉行,這一屆AI開發者大會有哪些亮點?一線公司的大牛們都在關注什麼?AI行業的風向是什麼?2019 AI開發者大會,傾聽大牛分享,聚焦技術實踐,和萬千開發者共成長。

目前,大會盲訂票限量發售中~掃碼購票,領先一步!

萬字長文詳解如何用 Python 玩轉 OpenGL | CSDN 博文精選萬字長文詳解如何用 Python 玩轉 OpenGL | CSDN 博文精選

相關推薦

推薦中...