OpenGL教程 学习笔记
文章目錄
- OpenGL教程
- 1 概念
- 1.1 是什么?
- 1.2 核心模式與固定渲染管線模式
- 1.3 狀態機
- 1.4 視口(Viewport)
- 1.5 渲染(**Render**):從3D點云到屏幕圖像的過程
- 1.6 著色器(Shader):處理數據的程序
- 2 基本內容
- 2.1 頂點輸入
- 2.2 頂點著色器(Vertex Shader)
- 2.3 片段著色器(Fragment Shader)
- 2.4 著色器程序(Program)
- 2.5 紋理
- 3 坐標變換
- 3.1 矩陣運算
- 3.1.1 向量相乘
- 點乘
- 叉乘
- 3.1.2 矩陣
- 3.2 坐標運算
- 3.2.1 局部空間
- 3.2.2 模型矩陣:局部——世界坐標
- 3.2.3 視圖矩陣:世界——觀察坐標
- 3.2.4 投影矩陣:觀察——裁剪空間坐標
- 3.2.5 正射投影
- 3.2.6 透視投影
- 3.3 攝像機
- 3.3.1 Look At
- 3.3.2 歐拉角
- 4 光照
- 4.1 光照模型
- 4.1.1 環境光照
- 4.1.2 漫反射光照
- 4.1.3 鏡面光照
- 4.2 材質
- 4.3 光源
- 4.3.1 平行光
- 4.3.2 點光源
- 4.3.3 聚光源
- 5 模型
- 5.1 網格
- 6 深度:遮擋
- 7 混合:透明度
- 8 面剔除:丟棄背向面
- 8.1 環繞順序
GLSL 語法https://blog.csdn.net/xhm01291212/article/details/79270836
python目錄結構 https://www.cnblogs.com/xiao-apple36/p/8884398.html
OpenGL教程
1 概念
1.1 是什么?
一般它被認為是一個API,包含了一系列可以操作圖形、圖像的函數。然而,OpenGL本身并不是一個API,它僅僅是一個規范。OpenGL規范嚴格規定了每個函數該如何執行,以及它們的輸出值。
1.2 核心模式與固定渲染管線模式
早期的OpenGL使用立即渲染模式(Immediate mode,也就是固定渲染管線),這個模式下繪制圖形很方便。OpenGL的大多數功能都被庫隱藏起來,開發者很少有控制OpenGL如何進行計算的自由。
而開發者迫切希望能有更多的靈活性,固定渲染管線效率太低。因此從OpenGL3.2開始,規范文檔開始廢棄立即渲染模式,并鼓勵開發者在OpenGL的核心模式(Core-profile)下進行開發,這個分支的規范完全移除了舊的特性。
1.3 狀態機
OpenGL自身是一個巨大的狀態機(State Machine):一系列的變量描述OpenGL此刻應當如何運行。OpenGL的狀態通常被稱為OpenGL上下文(Context)。我們通常使用如下途徑去更改OpenGL狀態:設置選項,操作緩沖。最后,我們使用當前OpenGL上下文來渲染。
假設當我們想告訴OpenGL去畫線段而不是三角形的時候,我們通過改變一些上下文變量來改變OpenGL狀態,從而告訴OpenGL如何去繪圖。一旦我們改變了OpenGL的狀態為繪制線段,下一個繪制命令就會畫出線段而不是三角形。
1.4 視口(Viewport)
OpenGL渲染窗口的尺寸大小,即視口(Viewport),通過調用glViewport函數來設置窗口的維度(Dimension):OpenGL幕后使用glViewport中定義的位置和寬高進行2D坐標的轉換,將OpenGL中的位置坐標轉換為你的屏幕坐標。
glViewport(0, 0, 800, 600); # 前兩個參數控制窗口左下角的位置。第三個和第四個參數控制渲染窗口的寬度和高度(像素)1.5 渲染(Render):從3D點云到屏幕圖像的過程
在OpenGL中,任何事物都在3D空間中,而屏幕和窗口卻是2D像素數組,這導致OpenGL的大部分工作都是關于把3D坐標轉變為適應你屏幕的2D像素。3D坐標轉為2D坐標的處理過程是由OpenGL的圖形渲染管線(Graphics Pipeline,大多譯為管線,實際上指的是一堆原始圖形數據途經一個輸送管道,期間經過各種變化處理最終出現在屏幕的過程)管理的。
圖形渲染管線可以被劃分為兩個主要部分:
-
第一部分把你的3D坐標轉換為2D坐標,
-
第二部分是把2D坐標轉變為實際的有顏色的像素。
1.6 著色器(Shader):處理數據的程序
圖形渲染管線可以被劃分為幾個階段,每個階段將會把前一個階段的輸出作為輸入。所有這些階段都是高度專門化的(它們都有一個特定的函數),并且很容易并行執行。正是由于它們具有并行執行的特性,當今大多數顯卡都有成千上萬的小處理核心,它們在GPU上為每一個(渲染管線)階段運行各自的小程序,從而在圖形渲染管線中快速處理你的數據。這些小程序叫做著色器(Shader)。OpenGL著色器是用OpenGL著色器語言(OpenGL Shading Language, GLSL)寫成的
- 頂點數據 Vertex Data
頂點數據(Vertex Data):以數組的形式傳遞3個3D坐標作為圖形渲染管線的輸入,用來表示一個三角形,這個數組叫做頂點數據(Vertex Data);頂點數據是一系列頂點的集合。一個頂點(Vertex)是一個3D坐標的數據的集合。
- 頂點屬性(Vertex Attribute):
而頂點數據是用頂點屬性(Vertex Attribute)表示的,它可以包含任何我們想用的數據,比如每個頂點由一個3D位置和一些顏色值組成
- 圖元(Primitive)
為了讓OpenGL知道我們的坐標和顏色值構成的到底是什么**,OpenGL需要你去指定這些數據所表示的渲染類型**。我們是希望把這些數據渲染成一系列的點?一系列的三角形?還是僅僅是一個長長的線?做出的這些提示叫做圖元(Primitive),任何一個繪制指令的調用都將把圖元傳遞給OpenGL。這是其中的幾個:GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP
2 基本內容
2.1 頂點輸入
開始繪制圖形之前,我們必須先給OpenGL輸入一些頂點數據。OpenGL是一個3D圖形庫,所以我們在OpenGL中指定的所有坐標都是3D坐標(x、y 和 z)。OpenGL僅當3D坐標在3個軸(x、y和z)上都為[-1.0,1.0]的范圍內時才處理它。所有在所謂的標準化設備坐標范圍內的坐標才會最終呈現在屏幕上
屏幕坐標系,(0, 0)坐標是這個圖像的中心,而不是左上角
-
X軸朝右
-
Y軸朝上
-
Z軸指向您后面,通常深度可以理解為z坐標,它代表一個像素在空間中和你的距離
由于我們希望渲染一個三角形,我們一共要指定三個頂點,每個頂點都有一個3D位置。定義為一個float數組。
float vertices[] = {-0.5f, -0.5f, 0.0f,0.5f, -0.5f, 0.0f,0.0f, 0.5f, 0.0f };1、標準化設備坐標——>屏幕空間坐標:glViewport視口變換(Viewport Transform)完成的。 2、屏幕空間坐標——>變換為片段——>片段著色器2.2 頂點著色器(Vertex Shader)
作用是坐標變換,輸出經過轉化之后的位置坐標
我們需要做的第一件事是用著色器語言GLSL(OpenGL Shading Language)編寫頂點著色器。
輸入:頂點坐標
輸出:坐標gl_Position和其它屬性 vec4(),四維坐標
attribute vec3 position; // 點云空間坐標 void main() {gl_Position = vec4(scale * position,1.0);// 注意最后一個分量是用在透視除法(Perspective Division)上。 }相關變量
-
uniform:一致變量,全局變量,對所有頂點或片斷都一樣
-
attribute:頂點屬性,每頂點不同
-
varying:可變變量,用于頂點、片斷著色器間傳遞自定義數據,在圖元裝配和光柵化過程,varying變量會被插值處理
2.3 片段著色器(Fragment Shader)
片段著色器所做的是計算像素最后的顏色輸出。在計算機圖形中顏色被表示為有4個元素的數組:紅色、綠色、藍色和alpha(透明度)分量,通常縮寫為RGBA。每個顏色分量的強度設置在[0.0,1.0]之間。
片段著色器需要一個vec4顏色輸出變量,因為片段著色器需要生成一個最終輸出的顏色。如果你在片段著色器沒有定義輸出顏色,OpenGL會把你的物體渲染為黑色(或白色)。
uniform是全局變量,我們可以在任何著色器中定義它們,而無需通過頂點著色器作為中介。
uniform vec4 color; void main() {// 片段著色器只需要一個輸出變量,這個變量是一個4分量向量,它表示的是最終的輸出顏色gl_FragColor = color; }2.4 著色器程序(Program)
著色器鏈接為一個著色器程序對象,然后在渲染對象的時候激活這個著色器程序。已激活著色器程序的著色器將在我們發送渲染調用的時候被使用。我們必須在渲染前指定OpenGL該如何解釋頂點數據,對應最后一個參數。為了讓OpenGL知道我們的坐標和顏色值構成的到底是什么**,OpenGL需要你去指定這些數據所表示的渲染類型**。做出的這些提示叫做圖元(Primitive)
gloo.Program(vertex, fragment, count=len(self.xyz)).draw(gl.GL_QUADS)2.5 紋理
紋理是一個2D圖片,它可以用來添加物體的細節;你可以想象紋理是一張繪有磚塊的紙,無縫折疊貼合到你的3D的房子上,這樣你的房子看起來就像有磚墻外表了。因為我們可以在一張圖片上插入非常多的細節,這樣就可以讓物體非常精細而不用指定額外的頂點。
為了能夠把紋理映射到三角形上,我們需要指定三角形的每個頂點各自對應紋理的哪個部分。這樣每個頂點就會關聯著一個紋理坐標,用來標明該從紋理圖像的哪個部分采樣。之后在圖形的其它片段上進行片段插值(Fragment Interpolation)
加載紋理
使用紋理之前要做的第一件事是把它們加載到我們的應用中。正確地加載圖像并生成一個紋理對象,在渲染之前先把它綁定到合適的紋理單元上:
earthTexture_01 = np.array(Image.open(".//resources//image//世界地圖4.jpg"))GPU上傳上傳點云紋理坐標、紋理圖像
program = gloo.Program(vertex, fragment, count=len(xyz)) program['texture'] = texture代碼中實現,在球類里面已經計算了紋理坐標
def sphere(r=1.0, m=100, n=100):"""計算球面點云坐標和紋理坐標:param r: 半徑:param m: 經線數:param n: 緯線數:return: 坐標和紋理坐標"""t = np.linspace(0, np.pi, m)p = np.linspace(0, 2 * np.pi, n)positions = []tex_positions = []normal = []for i in range(m - 1):for j in range(n - 1):x = r * np.sin(t[i]) * np.cos(p[j])y = r * np.sin(t[i]) * np.sin(p[j])z = r * np.cos(t[i])positions.append([x, y, z])tex_positions.append([p[j] / np.pi / 2, t[i] / np.pi]) # 就相當于把整個圖片按長分成2pi份,按高分成pi份x = r * np.sin(t[i + 1]) * np.cos(p[j])y = r * np.sin(t[i + 1]) * np.sin(p[j])z = r * np.cos(t[i + 1])positions.append([x, y, z])tex_positions.append([p[j] / np.pi / 2, t[i + 1] / np.pi])x = r * np.sin(t[i + 1]) * np.cos(p[j + 1])y = r * np.sin(t[i + 1]) * np.sin(p[j + 1])z = r * np.cos(t[i + 1])positions.append([x, y, z])tex_positions.append([p[j + 1] / np.pi / 2, t[i + 1] / np.pi])x = r * np.sin(t[i]) * np.cos(p[j + 1])y = r * np.sin(t[i]) * np.sin(p[j + 1])z = r * np.cos(t[i])positions.append([x, y, z])tex_positions.append([p[j + 1] / np.pi / 2, t[i] / np.pi])positions = np.array(positions)tex_positions = np.array(tex_positions)normal = np.array(positions)return positions, tex_positions, normal3 坐標變換
3.1 矩陣運算
3.1.1 向量相乘
兩個向量相乘是一種很奇怪的情況。普通的乘法在向量上是沒有定義的,因為它在視覺上是沒有意義的。但是在相乘的時候我們有兩種特定情況可以選擇:一個是點乘(Dot Product),記作vˉ?kˉvˉ?kˉ,另一個是叉乘(Cross Product),記作vˉ×kˉvˉ×kˉ。
點乘
兩個向量的點乘等于它們的數乘結果乘以兩個向量之間夾角的余弦值。可能聽起來有點費解,我們來看一下公式:
那么要計算兩個單位向量間的夾角,我們可以使用反余弦函數cos?1 ,可得結果是143.1度。現在我們很快就計算出了這兩個向量的夾角。點乘會在計算光照的時候非常有用。
叉乘
叉乘只在3D空間中有定義,它需要兩個不平行向量作為輸入,**生成一個正交于兩個輸入向量的第三個向量。**如果輸入的兩個向量也是正交的,那么叉乘之后將會產生3個互相正交的向量。接下來的教程中這會非常有用。下面的圖片展示了3D空間中叉乘的樣子:
兩個正交向量A和B叉積,輸出得到一個正交于兩個輸入向量的第三個向量
3.1.2 矩陣
數乘
現在我們也就能明白為什么這些單獨的數字要叫做**標量(Scalar)了。簡單來說,標量就是用它的值縮放(Scale)**矩陣的所有元素,上面中所有的元素都被放大了2倍。
單位矩陣
這種變換矩陣使一個向量完全不變:
縮放矩陣
如果我們把縮放變量表示為(S1,S2,S3)我們可以為任意向量(x,y,z)定義一個縮放矩陣:
位移矩陣
對于位移來說它們是第四列最上面的3個值。如果我們把位移向量表示為(Tx,Ty,Tz),我們就能把位移矩陣定義為:這才是把3維坐標變成四維坐標的原因:方便對左邊通過矩陣進行平移操作,有了位移矩陣我們就可以在3個方向(x、y、z)上移動物體,它是我們的變換工具箱中非常有用的一個變換矩陣。
旋轉矩陣
在3D空間中旋轉需要定義一個角和一個旋轉軸(Rotation Axis)。物體會沿著給定的旋轉軸旋轉特定角度。使用三角學,給定一個角度,可以把一個向量變換為一個經過旋轉的新向量。這通常是使用一系列正弦和余弦函數(一般簡稱sin和cos)各種巧妙的組合得到的。.轉半圈會旋轉360/2 = 180度,向右旋轉1/5圈表示向右旋轉360/5 = 72度。
矩陣的轉置
矩陣的逆
3.2 坐標運算
為了將坐標從一個坐標系變換到另一個坐標系,我們需要用到幾個變換矩陣,最重要的幾個分別是模型(Model)、觀察(View)、投影(Projection)三個矩陣。
-
局部坐標:以對象自己為中心,是對象相對于局部原點的坐標。
-
世界空間坐標:物體在一個更大的空間所處的坐標,可以確定與其他物體的相對位置,和其它物體一起相對于世界的原點進行擺放。
-
**觀察空間坐標:以觀察者為坐標,使得每個坐標都是從攝像機或者說觀察者的角度進行觀察的。**確定對于觀察者來講的相對位置關系
-
**投影坐標:**投影到裁剪坐標。裁剪坐標會被處理至-1.0到1.0的范圍內,并判斷哪些頂點將會出現在屏幕上。
-
**視口變換:**將裁剪坐標變換為屏幕坐標,視口變換將位于-1.0到1.0范圍的坐標變換到由glViewport函數所定義的坐標范圍內。最后變換出來的坐標將會送到光柵器,將其轉化為片段。
例如,當需要對物體進行修改的時候,在局部空間中來操作會更說得通;
如果要對一個物體做出一個相對于其它物體位置的操作時,在世界坐標系中來做這個才更說得通。
3.2.1 局部空間
局部空間是指物體所在的坐標空間,即對象最開始所在的地方。想象你在一個建模軟件(比如說Blender)中創建了一個立方體。你創建的立方體的原點有可能位于(0, 0, 0),你的模型的所有頂點都是在局部空間中:它們相對于你的物體來說都是局部的。
3.2.2 模型矩陣:局部——世界坐標
如果我們將我們所有的物體導入到程序當中,它們有可能會全擠在世界的原點(0, 0, 0)上,我們想為每一個物體定義一個位置,從而能在更大的世界當中放置它們。這就是你希望物體變換到的空間。物體的坐標將會從局部變換到世界空間;該變換是由模型矩陣(Model Matrix)實現的。
模型矩陣是一種變換矩陣,它能通過對物體進行位移、縮放、旋轉來將它置于它本應該在的位置或朝向。
你可以將它想像為變換一個房子,你需要先將它縮小(它在局部空間中太大了),并將其位移至郊區的一個小鎮,然后在y軸上往左旋轉一點以搭配附近的房子。你也可以把上一節將箱子到處擺放在場景中用的那個矩陣大致看作一個模型矩陣;我們將箱子的局部坐標變換到場景/世界中的不同位置。
glm.translate(m, self.orbit[0], self.orbit[1], self.orbit[2]) # 公轉, # translate表示瞬時坐標的位移變換經過怎樣的變換能得到單位矩陣,而這個返回的變換矩陣就是就得到模型矩陣 # 所以求model矩陣的思路是,算出局部坐標的瞬時位置,反推局部——世界的位移矩陣 # glm::translate() 創建一個位移矩陣,第一個參數是目標矩陣,第二個參數是位移的方向向量 # 返回的矩陣是能實現位移的矩陣,3.2.3 視圖矩陣:世界——觀察坐標
觀察空間就是從攝像機的視角所觀察到的空間。而這通常是由一系列的位移和旋轉的組合來完成,平移/旋轉場景從而使得特定的對象被變換到攝像機的前方。這些組合在一起的變換通常存儲在一個觀察矩陣(View Matrix)里,它被用來將世界坐標變換到觀察空間。
3.2.4 投影矩陣:觀察——裁剪空間坐標
為了將頂點坐標從觀察變換到裁剪空間,我們需要定義一個投影矩陣(Projection Matrix),它指定了一個范圍的坐標,比如在每個維度上的-1000到1000。投影矩陣接著會將在這個指定的范圍內的坐標變換為標準化設備坐標的范圍(-1.0, 1.0)。所有在范圍外的坐標不會被映射到在-1.0到1.0的范圍之間,所以會被裁剪掉
3.2.5 正射投影
效果不真實
定義了可見的坐標,它由由寬、高、近(Near)平面和遠(Far)平面所指定。任何出現在近平面之前或遠平面之后的坐標都會被裁剪掉。正射平截頭體直接將平截頭體內部的所有坐標映射為標準化設備坐標,因為每個向量的w分量都沒有進行改變;如果w分量等于1.0,透視除法則不會改變這個坐標
3.2.6 透視投影
透視(Perspective):離你越遠的東西看起來更小。
OpenGL要求所有可見的坐標都落在-1.0到1.0范圍內,作為頂點著色器最后的輸出,頂點坐標的每個分量都會除以它的w分量,距離觀察者越遠頂點坐標就會越小。這是也是w分量非常重要的另一個原因,它能夠幫助我們進行透視投影。最后的結果坐標就是處于標準化設備空間中的。
創建了一個定義了可視空間的大平截頭體,它的第一個參數定義了fov的值,它表示的是視野(Field of View),并且設置了觀察空間的大小。如果想要一個真實的觀察效果,它的值通常設置為45.0f。第二個參數設置了寬高比,由視口的寬除以高所得。第三和第四個參數設置了平截頭體的近和遠平面。我們通常設置近距離為0.1f,而遠距離設為100.0f。所有在近平面和遠平面內且處于平截頭體內的頂點都會被渲染。
perspective 函數使用
glm::perspective(float fovy, float aspect, float zNear, float zFar);
-
第一個參數為視錐上下面之間的夾角
-
第二個參數為寬高比,即視窗的寬/高
-
第三第四個參數分別為近截面和遠界面的深度
所以整體的坐標變換就是這樣
uniform float scale; // 模型縮放因子 uniform mat4 model; // 模型矩陣 uniform mat4 view; // 視圖矩陣 uniform mat4 projection; // 投影矩陣 uniform mat4 viewport; // 視口矩陣 attribute vec3 position; // 點云空間坐標void main(){gl_Position = viewport * projection * view * model * vec4(scale * position,1.0); }3.3 攝像機
當我們討論攝像機/觀察空間(Camera/View Space)的時候,是在討論以攝像機的視角作為場景原點時場景中所有的頂點坐標:觀察矩陣把所有的世界坐標變換為相對于攝像機位置與方向的觀察坐標。要定義一個攝像機,我們需要創建一個三個單位軸相互垂直的、以攝像機的位置為原點的坐標系。
攝像機位置
攝像機位置簡單來說就是世界空間中一個指向攝像機位置的向量。不要忘記正z軸是從屏幕指向你的,如果我們希望攝像機向后移動,我們就沿著z軸的正方向移動。
Z0 = 200 eyeAt = np.array([0, 0, Z0])攝像機方向
指的是攝像機指向哪個方向。讓攝像機指向場景原點:(0, 0, 0)。
用場景原點向量減去攝像機位置向量的結果就是攝像機的指向向量。由于我們知道攝像機指向z軸負方向,但我們希望方向向量(Direction Vector)指向攝像機的z軸正方向。如果我們交換相減的順序,我們就會獲得一個指向攝像機正z軸方向的向量:
lookAt = np.array([0, 0, 0]) # cam - target 就是獲得的攝像機方向方向向量(Direction Vector)并不是最好的名字,因為它實際上指向從它到目標向量的相反方向(譯注:注意看前面的那個圖,藍色的方向向量大概指向z軸的正方向,與攝像機實際指向的方向是正好相反的)。
右軸
它代表攝像機空間的x軸的正方向。把上向量和第二步得到的方向向量進行叉乘。兩個向量叉乘的結果會同時垂直于兩向量,因此我們會得到指向x軸正方向的那個向量
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f); glm::vec3 cameraRight = glm::normalize(glm::cross(up, cameraDirection));上軸
一個指向攝像機的正y軸向量,現在我們已經有了x軸向量和z軸向量,獲取一個指向攝像機的正y軸向量就相對簡單了:我們把右向量和方向向量進行叉乘:
eyeUp = np.array([0, 1, 0])3.3.1 Look At
將所有坐標變換到攝像機的視圖矩陣
現在我們有了3個相互垂直的軸和一個定義攝像機空間的位置坐標,用矩陣來表示這個坐標軸,用這個矩陣乘以任何向量來將其變換到那個坐標空間。這正是LookAt矩陣所做的
其中R是右向量,U是上向量,D是方向向量,P是攝像機位置向量。注意,位置向量是相反的,因為我們最終希望把世界平移到與我們自身移動的相反方向。**把這個LookAt矩陣作為觀察矩陣可以很高效地把所有世界坐標變換到剛剛定義的觀察空間。**LookAt矩陣就像它的名字表達的那樣:它會創建一個看著(Look at)給定目標的觀察矩陣。
def view(cam, tar, u): #u是上"""計算視圖矩陣:將世界坐標系的坐標變換到觀察坐標系,也就是相機的視圖當中"""cam = np.array(cam, np.float32)tar = np.array(tar, np.float32)u = np.array(u, np.float32)f = tar - cam # 攝像機方向,從原點指向攝像機的坐標f = f/np.linalg.norm(f) # 求求二范數,也就是模長u = u/np.linalg.norm(u) s = np.cross(f, u) # 返回兩個向量的叉積。 得到攝像機坐標的右軸u = np.cross(s, f) # 得到攝像機坐標的上軸R = np.array([[ s[0], s[1], s[2], 0], # R代表三維坐標軸[ u[0], u[1], u[2], 0],[-f[0],-f[1],-f[2], 0],[ 0, 0, 0, 1]]) T = np.array([[ 1, 0, 0, -cam[0]], # T代表相機的坐標[ 0, 1, 0, -cam[1]],[ 0, -0, 1, -cam[2]],[ 0, 0, 0, 1]])v2 = np.matmul(R, T) # 矩陣相乘既得到了lookAt矩陣也就是我們的觀察矩陣v2 = np.transpose(v2) # 轉置,行變列return v2在代碼中,我們是將LookAt矩陣直接賦值給view矩陣的,然后把view傳遞到vertex shader中。那么view矩陣是干嘛的呢?view是負責把世界坐標系轉換成用攝像機的視角所觀察到的坐標系當中。
LookAt
快速構建攝像機坐標系的方法(構建結果就是這個LookAt矩陣)
坐標系空間變換方法(世界空間->觀察空間)。用LookAt矩陣左乘某向量X,就可以將X從世界空間變換到觀察空間。
3.3.2 歐拉角
歐拉角(Euler Angle)是可以表示3D空間中任何旋轉的3個值,俯仰角是描述我們如何往上或往下看的角,可以在第一張圖中看到。第二張圖展示了偏航角,偏航角表示我們往左和往右看的程度。滾轉角代表我們如何翻滾攝像機,通常在太空飛船的攝像機中使用。每個歐拉角都有一個值來表示,把三個角結合起來我們就能夠計算3D空間中任何的旋轉向量了。
對于我們的攝像機系統來說,我們只關心俯仰角和偏航角,所以我們不會討論滾轉角。給定一個俯仰角和偏航角,我們可以把它們轉換為一個代表新的方向向量的3D向量。我們可以看到x分量取決于cos(yaw)的值,z值同樣取決于偏航角的正弦值。這樣我們就有了一個可以把俯仰角和偏航角轉化為用來自由旋轉視角的攝像機的3維方向向量了。
# 設置視點位置和方向 Z0 = 200 eyeAt = np.array([0, 0, Z0]) # cam lookAt = np.array([0, 0, 0]) # 目標衛星 eyeUp = np.array([0, 1, 0]) # 自轉軸 view_0 = myMath.view(eyeAt, lookAt, eyeUp)#計算視圖矩陣def view(cam, tar, u):"""計算視圖矩陣"""cam = np.array(cam, np.float32)tar = np.array(tar, np.float32)u = np.array(u, np.float32)f = tar - camf = f/np.linalg.norm(f)u = u/np.linalg.norm(u)s = np.cross(f, u)u = np.cross(s, f)R = np.array([[ s[0], s[1], s[2], 0],[ u[0], u[1], u[2], 0],[-f[0],-f[1],-f[2], 0],[ 0, 0, 0, 1]])T = np.array([[ 1, 0, 0, -cam[0]],[ 0, 1, 0, -cam[1]],[ 0, -0, 1, -cam[2]],[ 0, 0, 0, 1]])v2 = np.matmul(R, T)v2 = np.transpose(v2)# print(v2)return v24 光照
當我們在OpenGL中創建一個光源時,我們希望給光源一個顏色。我們將光源設置為白色。當我們把光源的顏色與物體的顏色值相乘,所得到的就是這個物體所反射的顏色(也就是我們所感知到的顏色),使用不同的光源顏色來顯現不同的顏色。
uniform vec3 objectColor; uniform vec3 lightColor; void main() {FragColor = vec4(lightColor * objectColor, 1.0); } def __init__(self, color=(1.0, 1.0, 0, 1.0)): # 構造器傳入self.fragment = """void main(){gl_FragColor = vec4"""+str(color)+""";} """4.1 光照模型
OpenGL的光照使用的是簡化的光照模型,對現實的情況進行近似,這樣處理起來會更容易一些,而且看起來也差不多一樣。這些光照模型都是基于我們對光的物理特性的理解。
比如馮氏光照模型(Phong Lighting Model)。馮氏光照模型的主要結構由3個分量組成:環境(Ambient)、漫反射(Diffuse)和鏡面(Specular)光照。下面這張圖展示了這些光照分量看起來的樣子:
- 環境光照(Ambient Lighting):即使在黑暗的情況下,世界上通常也仍然有一些光亮(月亮、遠處的光),所以物體幾乎永遠不會是完全黑暗的。為了模擬這個,我們會使用一個環境光照常量,它永遠會給物體一些顏色。
- 漫反射光照(Diffuse Lighting):模擬光源對物體的方向性影響(Directional Impact)。它是馮氏光照模型中視覺上最顯著的分量。物體的某一部分越是正對著光源,它就會越亮。
- 鏡面光照(Specular Lighting):模擬有光澤物體上面出現的亮點。鏡面光照的顏色相比于物體的顏色會更傾向于光的顏色。
4.1.1 環境光照
我們使用一個很小的常量(光照)顏色,添加到物體片段的最終顏色中,實現即便場景中沒有直接的光源也能看起來存在一些發散的光。
void main() {float ambientStrength = 0.1;vec3 ambient = ambientStrength * lightColor;vec3 result = ambient * objectColor;FragColor = vec4(result, 1.0); }4.1.2 漫反射光照
漫反射是指光線被粗糙表面無規則地向各個方向反射的現象。很多物體,如植物、墻壁、衣服等,其表面粗看起來似乎是平滑,但用放大鏡仔細觀察,就會看到其表面是凹凸不平的,所以本來是平行的太陽光被這些表面反射后,就彌漫地射向不同方向。
圖左上方有一個光源,它所發出的光線落在物體的一個片段上。為了測量光線和片段的角度,我們使用一個叫做**法向量(Normal Vector)**的東西,它是垂直于片段表面的一個向量(這里以黃色箭頭表示),這兩個向量之間的角度很容易就能夠通過點乘計算出來,我們知道兩個單位向量的夾角越小,它們點乘的結果越傾向于1。θ越大,光對片段顏色的影響就應該越小。
所以,計算漫反射光照需要什么?
- 法向量:一個垂直于頂點表面的向量。
- 定向的光線:作為光源的位置與片段的位置之間向量差的方向向量。為了計算這個光線,我們需要光的位置向量和片段的位置向量。
注意
目前片段著色器里的計算都是在世界空間坐標中進行的。所以,我們是不是應該把法向量也轉換為世界空間坐標?基本正確,但是這不是簡單地把它乘以一個模型矩陣就能搞定的。
首先,法向量只是一個方向向量,不能表達空間中的特定位置。同時,法向量沒有齊次坐標(頂點位置中的w分量)。這意味著,位移不應該影響到法向量。因此,如果我們打算把法向量乘以一個模型矩陣,我們就要從矩陣中移除位移部分,只選用模型矩陣左上角3×3的矩陣。對于法向量,我們只希望對它實施縮放和旋轉變換。
其次,如果模型矩陣執行了不等比縮放,頂點的改變會導致法向量不再垂直于表面了。因此,我們不能用這樣的模型矩陣來變換法向量。下面的圖展示了應用了不等比縮放的模型矩陣對法向量的影響:
每當我們應用一個不等比縮放時,法向量就不會再垂直于對應的表面了,這樣光照就會被破壞。
修復這個行為的訣竅是使用一個為法向量專門定制的模型矩陣。這個矩陣稱之為法線矩陣,大部分的資源都會將法線矩陣定義為應用到模型-觀察矩陣上的操作,但是由于我們只在世界空間中進行操作(不是在觀察空間),我們只使用模型矩陣。
Normal = mat3(transpose(inverse(model))) * aNormal;在漫反射光照部分,光照表現并沒有問題,這是因為我們沒有對物體本身執行任何縮放操作,所以并不是必須要使用一個法線矩陣,僅僅讓模型矩陣乘以法線也可以。可是,如果你進行了不等比縮放,使用法線矩陣去乘以法向量就是必不可少的了。
4.1.3 鏡面光照
鏡面光照也是依據光的方向向量和物體的法向量來決定的,當我們去看光被物體所反射的那個方向的時候,我們會看到一個高光。觀察向量是鏡面光照附加的一個變量,我們可以使用觀察者世界空間位置和片段的位置來計算它。之后,我們計算鏡面光強度,用它乘以光源的顏色,再將它加上環境光和漫反射分量。
下一步,我們計算視線方向向量,和對應的沿著法線軸的反射向量:
vec3 viewDir = normalize(viewPos - FragPos); vec3 reflectDir = reflect(-lightDir, norm);需要注意的是我們對lightDir向量進行了取反。reflect函數要求第一個向量是從光源指向片段位置的向量,但是lightDir當前正好相反,是從片段指向光源(由先前我們計算lightDir向量時,減法的順序決定)。為了保證我們得到正確的reflect向量,我們通過對lightDir向量取反來獲得相反的方向。第二個參數要求是一個法向量,所以我們提供的是已標準化的norm向量。
一個物體的反光度越高,反射光的能力越強,散射得越少,高光點就會越小。在下面的圖片里,你會看到不同反光度的視覺效果影響:
positions = np.array(positions) //非常奇怪?法向直接等于點云坐標? normal = np.array(positions) sun_direction = np.array(sat[0].orbit_xyz(Recorder.t)) - np.array(sat[1].orbit_xyz(Recorder.t)) //光的位置減片段在地心赤道春分坐標系中的衛星和相機瞬時坐標vertex = """ attribute vec3 normal; // 點云法向量 (1*3)varying vec3 v_normal; // 向片斷著色程序傳遞法向量void main(){vec4 n = model * vec4(normal,0.0); // model是模型矩陣,定義了自我中心坐標軸轉化成世界坐標的一系列變換v_normal = normalize(vec3(n[0],n[1],n[2]));//歸一化處理保證點乘結果就是夾角} """fragment = """varying vec3 v_normal; void main(){float cosine = max(0.0,dot(normalize(sun_direction),v_normal)); // 太陽光線的方向點乘法向就是夾角,點乘結果就是鏡面發射影響。結果值再乘以光的顏色,。兩個向量之間的角度越大,反射分量就會越小:float i_r = 3*(c[0]*cosine+0.00*c[0])/3.14; // c 代表的是color的太陽光數組float i_g = 3*(c[1]*cosine+0.00*c[1])/3.14; float i_b = 3*(c[2]*cosine+0.00*c[2])/3.14; color = vec4(i_r,i_g,i_b,1.0);gl_FragColor = color;} """4.2 材質
4.3 光源
4.3.1 平行光
當一個光源處于很遠的地方時,來自光源的每條光線就會近似于互相平行。不論物體和/或者觀察者的位置,看起來好像所有的光都來自于同一個方向。因為所有的光線都是平行的,所以物體與光源的相對位置是不重要的,因為對場景中每一個物體光的方向都是一致的。由于光的位置向量保持一致,場景中每個物體的光照計算將會是類似的。
我們可以定義一個光線方向向量而不是位置向量來模擬一個定向光。著色器的計算基本保持不變,但這次我們將直接使用光的direction向量而不是通過direction來計算lightDir向量。
sun_direction = np.array(Sun.orbit)當我們將位置向量定義為一個vec4時,很重要的一點是要將w分量設置為1.0,這樣變換和投影才能正確應用。然而,當我們定義一個方向向量為vec4的時候,我們不想讓位移有任何的效果(因為它僅僅代表的是方向),所以我們將w分量設置為0.0。
方向向量就會像這樣來表示:vec4(0.2f, 1.0f, 0.3f, 0.0f)。這也可以作為一個快速檢測光照類型的工具:你可以檢測w分量是否等于1.0,來檢測它是否是光的位置向量;w分量等于0.0,則它是光的方向向量,這樣就能根據這個來調整光照計算了:
if(lightVector.w == 0.0) // 注意浮點數據類型的誤差// 執行定向光照計算 else if(lightVector.w == 1.0)// 根據光源的位置做光照計算(與上一節一樣)4.3.2 點光源
點光源是處于世界中某一個位置的光源,它會朝著所有方向發光,但光線會隨著距離逐漸衰減。
4.3.3 聚光源
5 模型
我們不太能夠對像是房子、汽車或者人形角色這樣的復雜形狀手工定義所有的頂點、法線和紋理坐標。我們想要的是將這些模型(Model)導入(Import)到程序當中,模型通常都由3D藝術家在Blender、3DS Max或者Maya這樣的3D建模工具制作。這些工具將會在導出到模型文件的時候自動生成所有的頂點坐標、頂點法線以及紋理坐標。
5.1 網格
通常每個模型都由幾個子模型/形狀組合而成。組合模型的每個單獨的形狀就叫做一個網格(Mesh)。比如說有一個人形的角色:藝術家通常會將頭部、四肢、衣服、武器建模為分開的組件,并將這些網格組合而成的結果表現為最終的模型。一個網格是我們在OpenGL中繪制物體所需的最小單位(頂點數據、索引和材質屬性)。一個模型(通常)會包括多個網格。
一個網格應該至少需要一系列的頂點,每個頂點包含一個位置向量、一個法向量和一個紋理坐標向量。一個網格還應該包含用于索引繪制的索引以及紋理形式的材質數據(漫反射/鏡面光貼圖)。
6 深度:遮擋
判斷哪些是被遮擋的部分不應該被顯示
當深度測試(Depth Testing)被啟用的時候,OpenGL會將一個片段的深度值與深度緩沖的內容進行對比。OpenGL會執行一個深度測試,如果這個測試通過了的話,深度緩沖將會更新為新的深度值。如果深度測試失敗了,片段將會被丟棄。
gl_FragCoord的x和y分量代表了片段的屏幕空間坐標(其中(0, 0)位于左下角)。gl_FragCoord中也包含了一個z分量,它包含了片段真正的深度值。z值就是需要與深度緩沖內容所對比的那個值。
7 混合:透明度
OpenGL中,混合(Blending)通常是實現物體透明度(Transparency)的一種技術。透明就是說一個物體(或者其中的一部分)不是純色(Solid Color)的,它的顏色是物體本身的顏色和它背后其它物體的顏色的不同強度結合。一個有色玻璃窗是一個透明的物體,玻璃有它自己的顏色,但它最終的顏色還包含了玻璃之后所有物體的顏色。這也是混合這一名字的出處,我們混合(Blend)(不同物體的)多種顏色為一種顏色。所以透明度能讓我們看穿物體。
**一個物體的透明度是通過它顏色的aplha值來決定的。**Alpha顏色值是顏色向量的第四個分量,設置為1.0,讓這個物體的透明度為0.0,而當alpha值為0.0時物體將會是完全透明的。當alpha值為0.5時,物體的顏色有50%是來自物體自身的顏色,50%來自背后物體的顏色。
實現:丟棄(Discard)顯示紋理中透明部分的片段,不將這些片段存儲到顏色緩沖中。
8 面剔除:丟棄背向面
OpenGL能夠檢查所有面向(Front Facing)觀察者的面,并渲染它們,而丟棄那些背向(Back Facing)的面,節省我們很多的片段著色器調用。但我們仍要告訴OpenGL哪些面是正向面(Front Face),哪些面是背向面(Back Face)。OpenGL使用了一個很聰明的技巧,分析頂點數據的環繞順序(Winding Order)。
8.1 環繞順序
當我們定義一組三角形頂點時,我們會以特定的環繞順序來定義它們,可能是順時針(Clockwise)的,也可能是逆時針(Counter-clockwise)的。每個三角形由3個頂點所組成,我們會從三角形中間來看,為這3個頂點設定一個環繞順序。
觀察者所面向的所有三角形頂點就是我們所指定的正確環繞順序了,而立方體另一面的三角形頂點則是以相反的環繞順序所渲染的。這樣的結果就是,我們所面向的三角形將會是正向三角形,而背面的三角形則是背向三角形。通過這個順序能甄別面向還是背向
glCullFace函數有三個可用的選項:
- GL_BACK:只剔除背向面。
- GL_FRONT:只剔除正向面。
- GL_FRONT_AND_BACK:剔除正向面和背向面。
總結
以上是生活随笔為你收集整理的OpenGL教程 学习笔记的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JAVA模板模式,简历模板(例子)
- 下一篇: 用ANSYS画矩形_ANSYS软件使用的