從零開始實現3D軟光柵渲染器 (5-1) 3D渲染流水線(上)

什麼是渲染流水線

把大象放冰箱,需要幾步?

  1. 打開冰箱
  2. 放入大象
  3. 關上冰箱

再放一個呢?

  1. 打開冰箱
  2. 放入大象
  3. 關上冰箱

這就是流水線。

渲染流水線(裝逼的就叫渲染管線),其實解決了2個問題:

把3D物體顯示到2D屏幕,

  1. 3D空間的物體顯示在屏幕上什麼地方?
  2. 物體該顯示成什麼樣子?玻璃應該顯示成透明的吧?放在玻璃後面的物體應該能看到吧?水泥地面,草地,牆面看起來應該不一樣吧?總之,你顯示的東西要符合我們對世界的認知!

既然是流水線,那它的流程就相對固定。早期的OpenGL使用的是固定渲染管線,也就是說,它完全規定了物體渲染的若干個階段,這樣程序員就省事了,程序員只要調用相應的API給特定的渲染階段設置參數即可。但這樣的缺點也很明顯,就是不靈活呀。就像以前的直板機(現在的老人機),用起來很方便,反應速度快,但是你很難自己寫App啊,一般出廠的時候,手機中的程序都固化在電路板上了,用戶沒辦法自己擴展啊。但是,如今的Android系統,IOS系統,都提供了可編程的開發接口給用戶開發App,以增強手機的功能。OpenGL也是這樣,不知道在哪個版本之後,就開始提供可編程渲染管線,讓用戶可以自定義渲染的若干個階段,這樣做的好處就是,很靈活,用戶可以定製自己的需求,來實現固定渲染管線難以實現的效果,但是缺點就是對程序員的要求較高,因爲你要明白它是怎麼玩的你才能定製呀。在OpenGL3.0之後,就完全摒棄了固定渲染管線,全面支持可編程渲染管線。既然前者已經成爲歷史的遺物,而作爲新時代的社會主義接班人就沒必要再趟這趟混水了。就比如你歷經千辛萬古學會了屠龍之術,才發現這世上本沒有龍,這是何等的悲哀。我們直接學習可編程流水線吧,因爲舊的我也不會。

可編程渲染管線

下面是可編程渲染管線包含的幾個階段。

其中藍色框表示的階段,就是程序員可以定製的階段。

着色器(Shading Language),這玩意兒怎麼說呢? OpenGL中用的叫 GLSL,就是一小段程序代碼,在GPU上執行(因爲GPU強大的並行計算能力,可同時運行多個着色器程序代碼,進而提供圖形繪製的效率)。在可編程渲染管線中程序員可定製的階段就是在這些着色器中實現的。這裏我們簡單對這些概念有些瞭解即可,我們現在需要學習的渲染管線的各個階段到底幹了個啥,至於如何寫這些着色器程序,在學習OpenGL的時候會接觸到,其實也不是很難。現在大家知道有這些個階段就可以了。

頂點着色器中完成了從3D空間到二維屏幕的變換,也就是解決了我們前面說的第一個問題(三維空間的物體如何變換顯示到屏幕上)。這也是我們本節介紹的,第二個問題,我們後面再說。這個遠比第一個問題複雜得多。

順便說下,看到圖中光柵化的灰色框框沒有,這裏的光柵化就類似於前面幾節我們自己寫的光柵化點、直線,但是這裏的框框是灰色的,也就是你們程序員不能自己定製,這些算法由於過於成熟,都不配由程序員們來自己實現,都由顯卡廠商集成到顯卡中了。是的,你不配實現這些算法。啊???那前面不是白學了?對!從某種意義上,是的。但是,仁者見仁智者見智,做人不要太功利嘛,兄dei.

好了,開始進入正題。

將3D空間的點變換到屏幕空間,需要經歷以下階段:物體空間,世界空間,相機空間(觀察空間),裁剪空間,屏幕空間。不同的書上叫法不一樣,請知曉。

是不是有些暈?爲什麼搞這麼多“空間“,爲什麼需要經過這麼多步轉化,一步到位不行嗎?當然可以。但是,方便的東西一般靈活性都差,就像前面說的固定管線一樣,雖然很方便,但是它不靈活呀,你無法方便地定製自己的需求去實現一些特殊的效果。比如,後面說到的頂點着色,有在世界座標系中計算頂點顏色的,有在觀察座標系中計算頂點顏色的,兩者的效果是不一樣的。有人會說,當然選擇效果好的呀。但是效果好的需要好的顯卡呀,不是每個應用情景都能夠使用好的渲染效果的,比如手機的性能肯定不如PC。從某方面說,圖形學就是在效果和性能上的博弈。講這麼多廢話,就是想說大家學習知識的時候要多想爲什麼,死讀書讀死書,有的時候行,有的時候它不靈呀。

下面我們挨個介紹每個座標空間、涉及的變換,及其推導過程。

對象空間

又叫本地空間、物體空間、局部空間,還有我可能也沒聽過的什麼空間,反正都是指一個東西。

在圖形學中,大家把座標空間理解成對應的座標系即可,不同的座標空間,就是指不同的座標系。對象空間對應的座標系就是本地座標系(局部座標系?對象座標系?不管叫啥,你知道就行)。

一般遊戲場景中都有很多不同的模型(場景模型、人物模型、怪物模型、特效等),這些一般都不是一個人做的,也不一定是一個團隊做的。很多獨立遊戲工作室,像這些美術資源都是外包出去的。

大家都知道,3D建模的時候,你總得需要設置你建立的模型的原點以及模型的尺寸吧。不同人建模的習慣是不一樣的,除非你明確給人家提建模的規範,否則人家都是按照自己的建模習慣來。像下面blender中,默認的原點在物體的中心,有的人建模的時候,習慣將人物放在人的腰部,有的人習慣放在兩腳之間。此外,不同的人使用的建模尺寸(單位)也是不同的,有人使用米,有人使用釐米。這些數據都是在本地座標系中刻畫的。舉個例子,兩個不同的工作室給你2個人物模型A和B,同樣是(100,800)這個點,A模型可能落在模型的嘴巴上,B模型可能落在模型的額頭上。我想,大家都懂我的意思了吧。

那麼針對不同的模型我們怎麼統一操作呢?比如上面的王者地圖,我們怎麼把不同的模型放到合適的位置,且大小合適呢?這就需要將物體從本地座標系轉換到世界座標系。

世界空間

世界座標系就是描述你要構建的場景中的所有東西的。有的書上說它是唯一的、固定不動的座標系。但是我覺得吧,這是相對的,比如你做一個發生在教室裏的遊戲,整個遊戲的場景就是在教室裏,那你的世界座標系可能就是描述整個教室的場景,如果你做一個宇宙大冒險的遊戲,那你的世界座標系可能就是描述整個宇宙空間。我想大家應該懂這個意思了吧。

那麼如何將物體從局部座標系變換到世界座標系呢?我想,這個就太簡單了吧。這利用我們上一節說的基本變換矩陣不就可以了嗎?平移、縮放、旋轉。總之就是要使用世界座標系描述所有的物體,這樣你才能統一管理所有的物體,也爲了後面方便地對這些物體進行操作。至於平移到哪?旋轉多少?那當然根據你自己的需求。送分題!!!

觀察空間

這個空間很有意思。顧名思義,觀察空間當然是提供給人查看場景的。因爲世界那麼大,顯示器也就那麼大,你不可能一下子渲染所有的場景給觀察者。所以,觀察空間就是決定給用戶呈現哪些場景。

大家把你的應用程序窗口想象成一個相框,《清明上河圖》大家都聽說過吧,就是那個很長的一幅畫。

那麼如何在顯示器這個有限大小的相框中查看這麼長的一幅圖呢?

兩種方法:

  1. 把相框懟到圖上,扯下面的圖,那麼你在相框中就能看到不同的內容。
  2. 把圖固定住,在圖上滑動相框,那麼你也能在相框中看到不同的內容。

大家說,哪種方法方便,有人說,肯定第一種方法方便啊,扯下面的圖,我人就不用拿着相框動了呀;有人說,肯定第二種方法啊,相框就相當於一個相機,我對準哪裏就顯示哪裏呀,多方便呀。嗯,都有道理。懶,是科技創新的源泉。

我們先來看第二種方法,把圖固定住,移動相框(這裏也就是你的應用程序窗口)。大家想一個問題啊,我們玩遊戲的時候是不是都是在窗口中操作的,假設我們王者的地圖是在世界座標系中定位的,而世界座標系的默認原點在屏幕的中心(實際上也確實這麼幹的,因爲後續計算比較方便),如果我們通過移動窗口來觀察王者地圖的各個部分,那麼你的應用程序窗口中心位置還是世界座標系的原點嗎?是不是就不是了?有人會說,不是又怎樣?emmm…怎麼說呢,一般我們就是默認把世界座標系的原點放在窗口的正中心(後面介紹視口變換的時候會提到),大家都是這麼幹的。而且,世界座標系之所以叫“世界”,它的原點如果在窗口中的位置是變化的,叫它“世界”座標系是不是也不太貼切?

好了。那隻能選第一種咯。是的。不過選第一種的別高興太早。相框不動,扯底下的圖意味着我們需要對場景中的每個物體挨個進行變換,那麼你怎麼描述這個變換呢?你對着場景中的每個物體說,唉,左一點?不對,過了,回來一點。艾,上一點?有人說,笨啊,用矩陣啊,是的,基於上一節介紹的基本變換工具,我們可以構建任意複雜的變換。但是難道每次變換都需要我們自己手動構建平移矩陣、旋轉矩陣、縮放矩陣嗎?是不是有點麻煩了。

我們再想想,如果有個相機,我們只操作相機就能控制我們看到的畫面,是不是挺方便的。換句話說,我們通過變換相機進而達到變換場景中物體的目的。第二種方法中滑動相框是不是就類似這個方法?但是我們剛纔不是否定了第二種方法嗎? 是呀,但是我們可以構建個虛擬的相機,我們假裝移動窗口,實際上變換的是場景中的物體,這叫啥來着?隔山打牛?移花接木?反正就是那意思。虛擬相機的作用就是讓我們去操作它,進而可以根據需求以不同的視角來觀察場景。總之,世上本沒有相機, 只是我們引入了“相機”的概念,創建了一個虛擬的相機來間接變換場景中的物體,使其變換到窗口中合適的位置以供觀察。一句話總結,視圖變換是變換場景中的物體。

一般我們可以構建兩種類型的虛擬相機:

  • 歐拉相機
  • UVN相機

注意,這裏的相機和Unity中給的第一人稱相機,第三人稱相機,軌道相機等屬於不同層次上的相機。我們這裏說的相機是實現層次上的,是對虛擬相機的定義;而像第三人稱相機,是在應用層面構建的相機,是爲了方便觀察場景用的,它是基於歐拉相機或者UVN相機來構建的。

歐拉相機

歐拉相機,顧名思義就是以歐拉角定義的相機,或者說歐拉這個人首先提出的,不管怎麼樣,我們不要想得太複雜,一個名字而已。

歐拉相機的思想很簡單,就是構建一個變換矩陣,來變換場景中的物體到合適的位置。看下面這個圖,一般我們的世界座標系(注意,相機是在世界座標系中定義的)的原點默認放在屏幕(你的應用程序窗口)的中心(這個和後面的視口變換有關係,這都是潛規則,專家告訴你,放在窗口中心操作最方便,你最好這麼幹,這個後面再說)。

拍過照的同學都知道,你相機對着啥,你就能看到啥。圖中,相機正對着照片,發揮你的現象力,我們應該看到的一幅正立的在窗口中心的照片。但是現在你的窗口中什麼也看不到,因爲照片在世界座標系的第一象限且是旋轉過的呀。那麼我們如何才能看到相機中拍的效果呢?很簡單啊,前面說了,我們通過變換相機來間接變換場景中的物體。

那我們需要構建這樣一種變換:先轉正相機(就是使相機的局部座標系三個座標軸和世界座標系三個座標軸平行),再把相機移到世界座標系的原點。將這個變換施加到場景中的所有物體上,那麼我們就能看到我們構建的相機變換的效果了。

但是明眼人一眼就看出來了,這個相機就是個幌子呀,這不就是對場景中所有的物體,都做了一個統一的變換嘛。對呀,前面不是說了嘛,這個就是虛擬相機呀,它就是爲了方便對象場景中的物體進行變換,進而方便觀察場景的呀。這個大家自己想想是不是這麼回事,這個還是有點繞的。不要光記着我說的先這樣,再那樣,你們多想想爲什麼這麼幹,這麼幹的好處是什麼,不這麼幹的話不方便的地方在哪裏?多琢磨幾次,你就知道了。

下面是構建這個歐拉相機的僞代碼:

	// 這個Camera就是我們定義的歐拉相機,很簡單,它有自己的位置,有自己的局部座標系,它的旋轉角就是相對自己的本地座標系來定義的。
	public class Camera {
        private Vector3f position = new Vector3f(0, 0, 100);	// 在世界座標系中的位置
        private float pitch = 20f; 	// 繞x軸轉的角度 --> 俯仰角
        private float yaw;			// 繞y軸轉的角度 --> 偏航角
        private float roll;			// 繞z軸轉的角度 --> 翻滾角

        public Camera() {
        }
	}
	
	// 構建相機變換的矩陣
	public static Matrix4f createViewMatrix(Camera camera) {
		Matrix4f viewMatrix = new Matrix4f();
		viewMatrix.setIdentity();
		
		// ========== 1. 先旋轉 ===========
		// 繞x軸變換
		Matrix4f.rotate((float) Math.toRadians(camera.getPitch()),
				new Vector3f(1, 0, 0), viewMatrix, viewMatrix);
		// 繞y軸變換
		Matrix4f.rotate((float) Math.toRadians(camera.getYaw()), new Vector3f(
				0, 1, 0), viewMatrix, viewMatrix);
		// 繞z軸變換
		Matrix4f.rotate((float) Math.toRadians(camera.getRoll()), new Vector3f(
				0, 0, 1), viewMatrix, viewMatrix);
		
		// ========== 2. 再平移 ===========
		// 平移相機到世界座標系原點
		Vector3f cameraPos = camera.getPosition();
		Vector3f negativeCameraPos = new Vector3f(-cameraPos.x, -cameraPos.y,
				-cameraPos.z);
		Matrix4f.translate(negativeCameraPos, viewMatrix, viewMatrix);

		return viewMatrix;
	}

其實實現起來一點也不復雜,關鍵是初學的時候不太好理解。歐拉相機其實非常符合人的思考習慣,也最容易理解。

UVN相機

UVN相機和歐拉相機乾的事情是一樣的,只不過兩者定義相機的方式不同。前面是利用歐拉角定義的相機,UVN相機是利用相機的朝向、向上的方向以及右方向定義的。

大多數3D編程接口(OpenGL輔助庫,Three.js etc.)幾乎都提供一個類似 glLookAt 的函數,其實這個定義的就是UVN相機的變換矩陣。

UVN相機的思想是給一個目標點,只要相機對着這個目標點,那就能看到這個物體了。接着再調整相機的姿態進而調整看到物體的姿態。有人會問什麼是姿態?其實和上面歐拉相機做的變換差不多,一般介紹這個概念的書都會給一個飛機的模型,你把UVN相機看成飛機,飛機繞其自身座標系的三個軸旋轉就得到三種不同的姿態,繞x軸–俯仰,繞y軸–偏航,繞z軸–翻滾。

那麼UVN相機構建的變換矩陣是什麼樣的呢?

說這個問題之前,我們先來回顧一下上一節介紹的空間變換的相關知識。

這是我們上一節介紹的旋轉變換。

我們換個角度思考這個問題啊。假設A點座標(10,15),繞原點逆時針旋轉alpha之後,A點旋轉到A’點。

問題來了。

A點你說是 (10,15),你是相對哪個座標系來說的?很明顯,是原始的座標系(黑色的)。那麼(10,15)這個座標你是如何度量的?

看圖可知,一個矩陣可以表示一個座標空間,原始的座標系就是一個單位矩陣。我們旋轉物體,由於物體本身也有座標系,其局部座標系也會跟着旋轉。所以,我們旋轉物體有2種等價的描述:

  1. 原始座標系不變,旋轉物體到新的位置,新位置也是在原始座標系中描述
  2. 與之等價的是,物體不動,對座標系進行逆變換(反着方向旋轉),在變換後的座標系中描述物體。舉個例子:你和你女友站在馬路的兩端,她不動你奔過去,和你不動,她朝着相反的方向朝你奔過去,其效果是一樣的。

第二種也就是常說的座標系變換。記住:運動是相對的。

那麼如何描述新的座標系呢? 前面也說了,矩陣可以構建一個座標空間,那麼單位矩陣(原始座標系)順時針旋轉(和之前物體變換方法相反,逆變換)alpha角度後(新的座標系)得到什麼樣的矩陣呢?

看看,得到的矩陣是不是和上一節推導的是一致的?再一次證明了,運動是相對的。我想我說明白了吧。

那麼這個有什麼用呢?那這個可就太有用了。大家想想,我們虛構的虛擬相機是幹嘛的?是不是將世界座標系轉換到觀察座標系?既然是座標系,那它就能用一個矩陣表示啊,因爲前面說了矩陣能表示一個座標系空間啊。那我們這裏的UVN相機只要求出相機的局部座標系不就能構建這樣一個矩陣了嗎?

首先,要構建UVN相機你需要給一個目標點 target,一個相機的向上的方向 up,還有相機的位置 position (再嘮叨一下,相機是在世界座標系中定義的,即它的位置是世界座標系中的點)。

下面我們來求UVN相機的各個座標軸對應的向量:

  • 相機的前向向量:v = (position-target)

  • v 叉乘 up 我們可以得到垂直於v和up向量的右向量,我們叫R向量,然後再次 v 叉乘 R 向量,重新計算UVN相機的向上方向向量,因爲這個一開始我們默認給的向上向量一般是(0,1,0),即默認和世界座標系Y方向一致,這可能不準確,所以需要重新計算一次。

  • 將 v,up以及R向量組合成變換矩陣

  • 最後別忘了和之前歐拉相機一樣將UVN相機平移到世界座標系的原點,即乘以一個平移矩陣。

我想,我說明白了吧。好了,由於這節太長了,後面的我們下節續上。

由於最近比較忙,這節內容斷斷續續寫了很長時間,寫的過程中刪減了很多東西,儘可能寫的簡潔、通俗一點,讓大家看得明白一點。大家有任何問題,歡迎給我留言。

歡迎大家關注我的公衆號【OpenGL編程】,定期分享OpenGL相關的3D編程教程、算法、小項目。歡迎大家一起交流。

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章