最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

GPU 渲染管線與著色器 大白話總結(jié) ---- 一篇就夠

2023-12-05 06:58 作者:失傳技術(shù)  | 我要投稿


GPU 渲染管線與著色器 大白話總結(jié) ---- 一篇就夠

在逆境中蛻變

已于?2023-07-27 10:26:05?修改

閱讀量701

?收藏?10

點(diǎn)贊數(shù) 7

分類專欄:?UnityShader?游戲圖形學(xué)?Unity開發(fā)?文章標(biāo)簽:?圖形渲染

版權(quán)

UnityShader同時被 3 個專欄收錄

38 篇文章20 訂閱

訂閱專欄

游戲圖形學(xué)

4 篇文章1 訂閱

訂閱專欄

Unity開發(fā)

4 篇文章0 訂閱

訂閱專欄

轉(zhuǎn)載自:
https://blog.csdn.net/newchenxf/article/details/119803489
真的寫的非常不錯!大力推薦

GPU?渲染管線與著色器 大白話總結(jié) ---- 一篇就夠


文章目錄

  • GPU 渲染管線與著色器 大白話總結(jié) ---- 一篇就夠

  • 1 前言

  • 2 渲染管線


    • 2.1 CPU和GPU是如何并行工作的?

  • 3 CPU處理 ---- 應(yīng)用階段


    • 3.1.1 加載模型數(shù)據(jù)例子

    • 3.1.2 加載紋理的例子

    • 3.1 把數(shù)據(jù)加載到顯存


    • 3.2 設(shè)置渲染狀態(tài)

    • 3.3 調(diào)用Draw Call

    • 3.4 小結(jié)

  • 4 GPU處理 ----幾何階段


    • 4.3.1 模型空間

    • 4.3.2 世界空間

    • 4.3.3 觀察空間

    • 4.3.4 裁減空間

    • 4.3.5 屏幕空間

    • 4.1 頂點(diǎn)數(shù)據(jù)是什么

    • 4.2 頂點(diǎn)著色器

    • 4.3 坐標(biāo)空間


    • 4.4 頂點(diǎn)坐標(biāo)變換總結(jié)

    • 4.5 視頻播放的頂點(diǎn)數(shù)據(jù)回顧

  • 5 GPU處理 ---- 光柵化階段


    • 5.3.1 模板測試

    • 5.3.2 深度測試

    • 5.3.3 混合Blending

    • 5.3.4 圖像輸出-雙緩沖

    • 5.1 三角形設(shè)置與遍歷

    • 5.2 片元著色器

    • 5.3 逐片元操作


  • 6 性能討論


    • 6.2.1 Unity靜態(tài)批處理

    • 6.2.2 Unity動態(tài)批處理

    • 6.1 性能瓶頸在哪里

    • 6.2 性能如何優(yōu)化


  • 7 附錄知識


    • 7.1 什么是齊次坐標(biāo)

    • 7.2 平移矩陣

    • 7.3 縮放矩陣

    • 7.4 旋轉(zhuǎn)矩陣

    • 7.5 組合變換

  • 參考


1 前言

做圖形圖像,就要懂GPU;
要懂GPU,最重要是懂渲染管線(或者叫流水線);
而渲染管線,最重要的環(huán)節(jié)就是shader,即著色器。

所以本文嘗試總結(jié)GPU的渲染流程和shader,可作為圖像開發(fā)的入門教程。

2 渲染管線

管線,又稱流水線。

什么是流水線呢?

流水線是指在重復(fù)執(zhí)行一項(xiàng)任務(wù)時,把它細(xì)分成很多小任務(wù),讓這些小任務(wù)重疊執(zhí)行,來提高整體的運(yùn)行效率。

舉個栗子:
卸貨搬運(yùn)工,3個人從車上搬到店里。3個人分別是A, B, C。
A給B,B給C,C到店里。
A不需要等C放到店里,再開始下一次的搬運(yùn),而是C在搬的過程中,就可以開始搬第二個貨物了。

CPU的處理,其實(shí)也是用流水線技術(shù),它把一條指令的執(zhí)行,拆分成五個部分:取指令、解碼、取數(shù)據(jù),運(yùn)算和寫結(jié)果。然后流程跟上面的搬運(yùn)工差不多。

而渲染管線,就是根據(jù)一個三維場景中的頂點(diǎn)、紋理等信息,轉(zhuǎn)換成一張二維圖像。這個工作由CPU + GPU 共同完成。
通常把一個渲染流程分3個階段:
(1) 應(yīng)用階段
(2) 幾何階段
(3) 光柵化階段

應(yīng)用階段由CPU完成,幾何階段 和 光柵化階段 由GPU完成。

當(dāng)然了,既然是流水線,意味著3個階段的執(zhí)行是異步的。也就是,CPU執(zhí)行完了,不需要GPU執(zhí)行完才開始下一次調(diào)用。
另外,GPU執(zhí)行的幾何階段和光柵化階段,也細(xì)分了子任務(wù),也是使用流水線的技術(shù)。

2.1 CPU和GPU是如何并行工作的?

答案就是?命令緩沖區(qū)?。命令緩沖區(qū)包含了一個命令隊(duì)列。CPU添加命令,GPU讀取命令。
當(dāng)CPU需要渲染一些對象時,就向命令緩沖區(qū)添加命令;當(dāng)CPU完成一次渲染任務(wù)后,可以從緩沖區(qū)繼續(xù)取出命令并執(zhí)行。


綠色的【渲染模型】,就是我們說的?Draw Call?。
舉個一個openGL的例子:

GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);

  • 1

橙色的【改變渲染狀態(tài)】,就是加載數(shù)據(jù),改變著色器,切換紋理啥的,舉個openGL的例子:

GLES20.glUseProgram(program); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);

  • 1

  • 2

一幀圖像的生成,如果有多個物體模型,就挨個提交DrawCall。GPU挨個繪制到顏色緩沖區(qū),然后混合,注意,雖然說是GPU挨個處理DrawCall,但是GPU內(nèi)部也是流水線,不需要等A繪制完了才繪制B,A只要完成了第一小步,進(jìn)入第二步,B就可以開始第一步了,這第一步就是下面會說的頂點(diǎn)著色器。

繪制一幀完成后,調(diào)用swapBuffer,把在顏色緩沖區(qū)的數(shù)據(jù),提交到屏幕顯示。

3 CPU處理 ---- 應(yīng)用階段

主要工作:
(1) 把數(shù)據(jù)加載到顯存(GPU的內(nèi)存)
(2) 設(shè)置渲染狀態(tài)
(3) 調(diào)用Draw Call

3.1 把數(shù)據(jù)加載到顯存

GPU一般不能直接訪問內(nèi)存,所以自己就搞了個內(nèi)存,叫顯存(顯卡內(nèi)存)。CPU把一些渲染所需數(shù)據(jù)從硬盤或網(wǎng)絡(luò)加載到內(nèi)存,然后送到顯存。
數(shù)據(jù)最主要就兩個,模型數(shù)據(jù)(頂點(diǎn)坐標(biāo)+紋理坐標(biāo))和紋理圖像。其他的數(shù)據(jù)量都很小,如法線方向。加載到顯存后,在CPU的內(nèi)存中的數(shù)據(jù),可以刪除,例如用bitmap生成一個紋理,加載到GPU后就可以回收了。

3.1.1 加載模型數(shù)據(jù)例子

下面用一個android視頻播放渲染的例子,來說明如何加載模型數(shù)據(jù)。

視頻是在一個二維的矩形框內(nèi)顯示,所以只需要4個頂點(diǎn)坐標(biāo),以及對應(yīng)的4個紋理坐標(biāo)。紋理用的是視頻解碼后的圖像。

? ?//頂點(diǎn)數(shù)據(jù) ? ?private float[] vertexData = { ? ? ? ? ? ?-1f, -1f, ? ? ? ? ? ?1f, -1f, ? ? ? ? ? ?-1f, 1f, ? ? ? ? ? ?1f, 1f ? ?}; ? ?//紋理坐標(biāo)數(shù)據(jù) ? ?private float[] fragmentData = { ? ? ? ? ? ?0f, 0f, ? ? ? ? ? ?1f, 0, ? ? ? ? ? ?0f, 1f, ? ? ? ? ? ?1f, 1f ? ?}; public void onCreate() { //頂點(diǎn)坐標(biāo)數(shù)據(jù)在JVM的內(nèi)存,復(fù)制到系統(tǒng)內(nèi)存 ? ? ? ?vertexBuffer = ByteBuffer.allocateDirect(vertexData.length * 4) ? ? ? ? ? ? ? ?.order(ByteOrder.nativeOrder()) ? ? ? ? ? ? ? ?.asFloatBuffer() ? ? ? ? ? ? ? ?.put(vertexData); ? ? ? ?vertexBuffer.position(0); ? ? ? ?//紋理坐標(biāo)數(shù)據(jù)在JVM的內(nèi)存,復(fù)制到系統(tǒng)內(nèi)存 ? ? ? ?fragmentBuffer = ByteBuffer.allocateDirect(fragmentData.length * 4) ? ? ? ? ? ? ? ?.order(ByteOrder.nativeOrder()) ? ? ? ? ? ? ? ?.asFloatBuffer() ? ? ? ? ? ? ? ?.put(fragmentData); //VBO全名頂點(diǎn)緩沖對象(Vertex Buffer Object),他主要的作用就是可以一次性的發(fā)送一大批頂點(diǎn)數(shù)據(jù)到顯卡上 int [] vbos = new int[1]; ? ? ? ?GLES20.glGenBuffers(1, vbos, 0); ? ? ? ?vboId = vbos[0]; //下面四句代碼,把內(nèi)存的數(shù)據(jù)復(fù)制到顯存中 ? ? ? ?GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vboId); ? ? ? ?GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, vertexData.length * 4 + fragmentData.length * 4, null, GLES20. GL_STATIC_DRAW); ? ? ? ?GLES20.glBufferSubData(GLES20.GL_ARRAY_BUFFER, 0, vertexData.length * 4, vertexBuffer); ? ? ? ?GLES20.glBufferSubData(GLES20.GL_ARRAY_BUFFER, vertexData.length * 4, fragmentData.length * 4, fragmentBuffer); }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

  • 20

  • 21

  • 22

  • 23

  • 24

  • 25

  • 26

  • 27

  • 28

  • 29

  • 30

  • 31

  • 32

  • 33

  • 34

  • 35

  • 36

  • 37

  • 38

  • 39

  • 40

代碼中已經(jīng)加了注釋,不過這里還是可以再說明一下。

vertexData
畢竟是java定義的變量,使用的JAVA虛擬機(jī)的內(nèi)存,而不是系統(tǒng)內(nèi)存,沒辦法直接給GPU的。所以,先用ByteBuffer,把vertexData拷貝到系統(tǒng)內(nèi)存。

接著,創(chuàng)建一個頂點(diǎn)緩沖對象(Vertex Buffer
Object),他主要的作用就是可以一次性的發(fā)送一大批頂點(diǎn)數(shù)據(jù)到顯卡上。然后,調(diào)用glBufferData,把數(shù)據(jù)拷貝到顯卡內(nèi)存上!這樣后面GPU開始工作時,就可以快速訪問了。

3.1.2 加載紋理的例子

這里再貼一個android app加載紋理圖像的例子:

? ?public static int createTexture(Bitmap bitmap){ ? ? ? ?int[] texture=new int[1]; ? ? ? ?//生成紋理 ? ? ? ?GLES20.glGenTextures(1,texture,0); ? ? ? ?//生成紋理 ? ? ? ?GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,texture[0]); ? ? ? ?//設(shè)置縮小過濾為使用紋理中坐標(biāo)最接近的一個像素的顏色作為需要繪制的像素顏色 ? ? ? ?GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_NEAREST); ? ? ? ?//設(shè)置放大過濾為使用紋理中坐標(biāo)最接近的若干個顏色,通過加權(quán)平均算法得到需要繪制的像素顏色 ? ? ? ?GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_LINEAR); ? ? ? ?//設(shè)置環(huán)繞方向S,截取紋理坐標(biāo)到[1/2n,1-1/2n]。將導(dǎo)致永遠(yuǎn)不會與border融合 ? ? ? ?GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,GLES20.GL_CLAMP_TO_EDGE); ? ? ? ?//設(shè)置環(huán)繞方向T,截取紋理坐標(biāo)到[1/2n,1-1/2n]。將導(dǎo)致永遠(yuǎn)不會與border融合 ? ? ? ?GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,GLES20.GL_CLAMP_TO_EDGE); ? ? ? ?if(bitmap!=null&&!bitmap.isRecycled()){ ? ? ? ? ? ?//根據(jù)以上指定的參數(shù),生成一個2D紋理,上傳到GPU ? ? ? ? ? ?GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0); ? ? ? ?} ? ? ? ?return texture[0]; ? ?}

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

  • 20

  • 21

  • 22

先用glGenTextures生成一個紋理id。這時候,還沒有創(chuàng)建實(shí)際數(shù)據(jù)。
接著,設(shè)置一下紋理的屬性,然后,調(diào)用texImage2D,這會先創(chuàng)建一個紋理buffer,然后把bitmap拷貝到buffer上。然后完事。上面的函數(shù)結(jié)束后,bitmap的內(nèi)容就拷貝到GPU了,可以根據(jù)需要回收。

需要強(qiáng)調(diào)的是,?不管是加載頂點(diǎn)還是紋理,都只需要在初始化時,一次性完成?,不需要每次onDraw都加載,否則就沒有意義了。

3.2 設(shè)置渲染狀態(tài)

即聲明場景的網(wǎng)格(模型)如何被渲染,比如用哪個vertex shader/fragment
shader,光源屬性,材質(zhì)(紋理)等。如果繪制不同的網(wǎng)格時沒有切換渲染狀態(tài),那么前后的網(wǎng)格,將使用同一種渲染狀態(tài)。

3.3 調(diào)用Draw Call

前面已經(jīng)提到了DrawCall,可能你已經(jīng)明白了,其實(shí)就是一個命令,發(fā)起方是CPU,接收方是GPU。
當(dāng)一個Draw Call調(diào)用時,GPU就會根據(jù)渲染狀態(tài)(材質(zhì),紋理,著色器)和所有的頂點(diǎn)數(shù)據(jù)進(jìn)行計(jì)算,GPU流水線開始運(yùn)轉(zhuǎn)。

再次強(qiáng)調(diào),

3.4 小結(jié)

第一步只需要1次加載就夠了,不用每次繪制都加載。 第二和第三,需要每次onDraw時設(shè)置。
這里也用繪制視頻或圖片的onDraw函數(shù),來舉個栗子:

public void onDraw(int textureId) ? ?{ ? ? ? ?GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); ? ? ? ?GLES20.glClearColor(1f,0f, 0f, 1f); ? ? ? ?//設(shè)置渲染狀態(tài): program根據(jù)具體的shader編譯,這里就是指定用哪個shader ? ? ? ?GLES20.glUseProgram(program); ? ? ? ?//設(shè)置渲染狀態(tài):指定紋理 ? ? ? ?GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId); ? ? ? ?//設(shè)置渲染狀態(tài):綁定VBO ? ? ? ?GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vboId); ? ? ? ?//設(shè)置渲染狀態(tài):指定頂點(diǎn)坐標(biāo)在VBO的從0開始,8個數(shù)據(jù) ? ? ? ?GLES20.glEnableVertexAttribArray(vPosition); ? ? ? ?GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 8, ? ? ? ? ? ? ? ?0); ? ? ? ?//設(shè)置渲染狀態(tài):指定頂點(diǎn)坐標(biāo)在VBO的從8*4(float是4個字節(jié))開始,8個數(shù)據(jù) ? ? ? ?GLES20.glEnableVertexAttribArray(fPosition); ? ? ? ?GLES20.glVertexAttribPointer(fPosition, 2, GLES20.GL_FLOAT, false, 8, ? ? ? ? ? ? ? ?vertexData.length * 4); ? ? ? ?//調(diào)用Draw Call, GPU 管線開始工作 ? ? ? ?GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); ? ? ? ?//繪制完成,恢復(fù)現(xiàn)場 ? ? ? ?GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); ? ? ? ?GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); ? ?}

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

  • 20

  • 21

  • 22

  • 23

  • 24

  • 25

  • 26

  • 27

  • 28

4 GPU處理 ----幾何階段

可以把幾何階段和光柵化階段放在一起,畫一張圖。


當(dāng)然了,上圖可能不全,中間可能還有些可選的步驟,我只是把最重要的列出來。
其中,藍(lán)色部分的頂點(diǎn)著色器,和片元著色器,是開發(fā)者完全可自定義編程的,也是大部分圖像開發(fā)同學(xué)需要關(guān)心的2個步驟。

4.1 頂點(diǎn)數(shù)據(jù)是什么

先來個基本說明:GPU認(rèn)識的數(shù)據(jù),只有點(diǎn),線,三角形。
這對應(yīng)1個頂點(diǎn),2個頂點(diǎn),3個頂點(diǎn)。這三個又稱為?圖元?。點(diǎn)和線一般在2D場景使用,在3D場景,基本是由N多個三角形,來拼接成一個物體模型。

下圖是一個人物模型的例子:


放大一些細(xì)節(jié),發(fā)現(xiàn)都是三角形組合而成。而三角形的頂點(diǎn),就是我們說的頂點(diǎn)坐標(biāo)。


所有的三角形貼上二維圖片紋理后,就是一個完整的圖像:


所以說,?頂點(diǎn)數(shù)據(jù),一般會包含頂點(diǎn)坐標(biāo) + 紋理坐標(biāo)。

一個模型文件,一般會包含頂點(diǎn)坐標(biāo),紋理坐標(biāo),紋理本身(紋理可以有多張)等。舉個栗子,打開3D max的obj后綴的模型文件,就可以看到如下文本內(nèi)容:

4.2 頂點(diǎn)著色器

頂點(diǎn)著色器是GPU內(nèi)部流水線的第一步。

它的處理單位是頂點(diǎn),也就是?每個頂點(diǎn),都會調(diào)用一次頂點(diǎn)著色器?。
它的主要工作:坐標(biāo)轉(zhuǎn)換和逐頂點(diǎn)光照,以及準(zhǔn)備后續(xù)階段(如片元著色器)的數(shù)據(jù)。

坐標(biāo)轉(zhuǎn)換,即把頂點(diǎn)坐標(biāo)從模型坐標(biāo)空間轉(zhuǎn)換到齊次裁切坐標(biāo)空間。

這里給一個Unity的shader代碼,Unity把頂點(diǎn)著色器和片元著色器的代碼,放到了一個文件中,不過Unity引擎會解析文件,轉(zhuǎn)換2個著色器代碼段,傳遞給GPU。

Shader "Unlit/SimpleUnlitTexturedShader" { ? ?Properties ? ?{ ? ? ? ?// 我們已刪除對紋理平鋪/偏移的支持, ? ? ? ?// 因此請讓它們不要顯示在材質(zhì)檢視面板中 ? ? ? ?[NoScaleOffset] _MainTex ("Texture", 2D) = "white" {} ? ?} ? ?SubShader ? ?{ ? ? ? ?Pass ? ? ? ?{ ? ? ? ? ? ?CGPROGRAM ? ? ? ? ? ?// 使用 "vert" 函數(shù)作為頂點(diǎn)著色器 ? ? ? ? ? ?#pragma vertex vert ? ? ? ? ? ?// 使用 "frag" 函數(shù)作為像素(片元)著色器 ? ? ? ? ? ?#pragma fragment frag ? ? ? ? ? ?// 頂點(diǎn)著色器輸入 ? ? ? ? ? ?struct appdata ? ? ? ? ? ?{ ? ? ? ? ? ? ? ?float4 vertex : POSITION; // 頂點(diǎn)位置 ? ? ? ? ? ? ? ?float2 uv : TEXCOORD0; // 紋理坐標(biāo) ? ? ? ? ? ?}; ? ? ? ? ? ?// 頂點(diǎn)著色器輸出("頂點(diǎn)到片元") ? ? ? ? ? ?struct v2f ? ? ? ? ? ?{ ? ? ? ? ? ? ? ?float2 uv : TEXCOORD0; // 紋理坐標(biāo) ? ? ? ? ? ? ? ?float4 vertex : SV_POSITION; // 裁剪空間位置 ? ? ? ? ? ?}; ? ? ? ? ? ?// 頂點(diǎn)著色器 ? ? ? ? ? ?v2f vert (appdata v) ? ? ? ? ? ?{ ? ? ? ? ? ? ? ?v2f o; ? ? ? ? ? ? ? ?// 將位置轉(zhuǎn)換為裁剪空間 ? ? ? ? ? ? ? ?//(乘以模型*視圖*投影矩陣) ? ? ? ? ? ? ? ?o.vertex = mul(UNITY_MATRIX_MVP, v.vertex); ? ? ? ? ? ? ? ?// 僅傳遞紋理坐標(biāo) ? ? ? ? ? ? ? ?o.uv = v.uv; ? ? ? ? ? ? ? ?return o; ? ? ? ? ? ?} ? ? ? ? ? ? ? ? ? ? ? ?// 我們將進(jìn)行采樣的紋理 ? ? ? ? ? ?sampler2D _MainTex; ? ? ? ? ? ?// 像素著色器;返回低精度("fixed4" 類型) ? ? ? ? ? ?// 顏色("SV_Target" 語義) ? ? ? ? ? ?fixed4 frag (v2f i) : SV_Target ? ? ? ? ? ?{ ? ? ? ? ? ? ? ?// 對紋理進(jìn)行采樣并將其返回 ? ? ? ? ? ? ? ?fixed4 col = tex2D(_MainTex, i.uv); ? ? ? ? ? ? ? ?return col; ? ? ? ? ? ?} ? ? ? ? ? ?ENDCG ? ? ? ?} ? ?} }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

  • 20

  • 21

  • 22

  • 23

  • 24

  • 25

  • 26

  • 27

  • 28

  • 29

  • 30

  • 31

  • 32

  • 33

  • 34

  • 35

  • 36

  • 37

  • 38

  • 39

  • 40

  • 41

  • 42

  • 43

  • 44

  • 45

  • 46

  • 47

  • 48

  • 49

  • 50

  • 51

  • 52

  • 53

  • 54

  • 55

  • 56

  • 57

  • 58

  • 59

代碼已經(jīng)加了清晰的注釋。
appdata就是頂點(diǎn)著色器的輸入,包括頂點(diǎn)坐標(biāo)和紋理坐標(biāo)。
通常,頂點(diǎn)著色器只處理頂點(diǎn)坐標(biāo),進(jìn)行空間轉(zhuǎn)換。 紋理坐標(biāo),一般透傳給片元著色器。

vert函數(shù)就是頂點(diǎn)著色器的代碼。Unity引擎會翻譯成類似如下的,帶有main函數(shù)的GLSL代碼,然后去編譯:

attribute vec4 v_Position; attribute vec4 f_Position; uniform mat4 uMVPMatrix; varying vec2 textureCoordinate; void main() { ? ?gl_Position = uMVPMatrix* v_Position; ? ?textureCoordinate = f_Position.xy; }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

空間轉(zhuǎn)換代碼竟然及其簡單,就是坐標(biāo)乘于一個MVP矩陣!

接下來,我們就基于?MVP矩陣?,來展開說一下空間轉(zhuǎn)換的概念。

在開始前,先強(qiáng)調(diào)一下,?main函數(shù)會執(zhí)行幾次?
答案是,有幾個頂點(diǎn),就執(zhí)行幾次,且是并行的!

4.3 坐標(biāo)空間

什么是坐標(biāo)空間呢?其實(shí)他在我們生活中處處存在。例如,你跟朋友約定在博物館大門右轉(zhuǎn)100米處見面,這時候,你說的位置,是一個以博物館的大門為原點(diǎn)的空間。所以坐標(biāo)空間,是一個相對的概念。

在游戲世界中也一樣。

根據(jù)不同的參照物,可以分為?模型空間-世界空間-觀察空間-裁剪空間-屏幕空間。

在3D世界中,頂點(diǎn)坐標(biāo)有3個數(shù)據(jù),是(x, y, z),但為了方便做平移/旋轉(zhuǎn)/縮放等轉(zhuǎn)換,需要補(bǔ)充一個w分量。即(x, y, z,
w),把這個4維的坐標(biāo),稱為齊次坐標(biāo)。這些知識,見第五節(jié)的?附錄知識?。

接下來,我們來挨個討論這些空間。

4.3.1 模型空間

你在3D max制作了一個人物模型,該模型可以放到各種軟件中適配。
對模型內(nèi)部來說,有一個坐標(biāo)原點(diǎn),比如就在心臟處,那身體各個部位相對于心臟,就有一個坐標(biāo)值;
每個模型對象,都有自己獨(dú)立的坐標(biāo)空間。所以也可以稱為?對象空間?或?局部空間?。

4.3.2 世界空間

可以把游戲中的一個地圖,作為一個小小的世界,地圖的中心,可以作為坐標(biāo)原點(diǎn)。人物模型作為一個整體,放到地圖中,就有了相對地圖中心的一個坐標(biāo)值。這就是世界空間。

把模型空間的坐標(biāo),轉(zhuǎn)換到世界空間的坐標(biāo),可以通過矩陣運(yùn)算得到。這個矩陣叫?Model Matrix?。
事實(shí)上,一個模型放到地圖中,可能會先縮放,然后旋轉(zhuǎn)或者平移。這3者都有對應(yīng)的矩陣,具體見?附錄?。

換言之,這個矩陣是由物體在地圖中的縮放,旋轉(zhuǎn),平移參數(shù)決定的。
換算的目的,就是得到物體的某個頂點(diǎn),在地圖中是個什么具體位置。

4.3.3 觀察空間

也可以稱為?攝像機(jī)空間
。觀察空間指的是,游戲地圖那么大,我們只能看到一部分內(nèi)容。于是在游戲地圖中,定義一個Camera,它也處于世界空間中。camera就相當(dāng)于我們的眼睛,camera照到哪里,哪里就是我們看到的畫面。

在3D游戲中,經(jīng)常就把Camera和用戶角色綁在一起。角色對象走到哪,Camera對象就移動到哪,效果便是,走到哪,就看到哪里的風(fēng)景!

所以地圖中的任何物體,如果把Camera作為坐標(biāo)原點(diǎn),也有一個相對于Camera的坐標(biāo)值。以Camera為原點(diǎn)的空間,就是觀察空間了。

把世界空間的坐標(biāo),換算到觀察空間,也可以通過矩陣運(yùn)算得到。這個矩陣叫?View Matrix?。

這個矩陣依賴什么參數(shù)呢?

首先,攝像機(jī)本身也位于世界坐標(biāo)系中,如果不考慮攝像機(jī)往哪里看,那其實(shí)就是很簡單的平移矩陣。
即,攝像機(jī)A坐標(biāo)如果是(-3, 0, 0), 那攝像機(jī)就是從世界原點(diǎn)O平移(-3, 0,
0)的結(jié)果。如果把攝像機(jī)的坐標(biāo)作為新的原點(diǎn),那原來的世界原點(diǎn)O,相當(dāng)于從攝像機(jī)平移(3, 0, 0)的結(jié)果。所以兩個坐標(biāo)系的轉(zhuǎn)換矩陣,近乎是一個平移矩陣。

當(dāng)然了,實(shí)際攝像機(jī)還有朝向,以及眼睛是正面看,還是倒立看。不知道啥是倒立看?看下圖

所以這個矩陣總用有三個因素影響。
來,一個函數(shù)解決煩惱,忘記復(fù)雜的計(jì)算過程。

glm::mat4 CameraMatrix = glm::LookAt( ? ?cameraPosition, // the position of your camera, in world space ? ?cameraTarget, ? // where you want to look at, in world space ? ?upVector ? ? ? ?// probably glm::vec3(0,1,0), but (0,-1,0) would make you looking upside-down, which can be great too );

  • 1

  • 2

  • 3

  • 4

  • 5

4.3.4 裁減空間

裁減空間就是攝像機(jī)能看到的區(qū)域。這個區(qū)域成為?視椎體?。
它由6個平面組成,這些平面也成為?裁減平面?。?視椎體?有兩種類型,涉及兩種投影方式。一個是?透視投影?,一個是?正交投影

完全位于這塊空間的圖元,會被保留;
完全位于空間之外的圖元,完全丟棄;
和空間邊界相交的圖元,會被裁減。

下圖是一個透視投影的示意圖。人物完全在視椎體內(nèi),所以右下角的小圖,是最終用戶看到的2D畫面樣子。


當(dāng)然了,圖中的Near 和Far,我是為了顯示方便,設(shè)置了比較小的值。實(shí)際游戲中,Near和Far差別很大,比如Near = 0.1, Far =
1000。這跟人眼可看的范圍是差不多的,近到貼眼睛的東西看不見,非常遠(yuǎn)的東西,也看不見。

那么,如何判斷一個東西就在視椎體內(nèi)呢?
答案是通過一個投影矩陣,把頂點(diǎn)轉(zhuǎn)換到一個裁減空間。
這個矩陣,基本上由攝像機(jī)的參數(shù)決定。

參數(shù)示意圖如下:

FOV代表視角的度數(shù)(Field of View);
Near和Far是攝像機(jī)原點(diǎn),距離裁切面的距離;
AspectRatio是裁切面的寬高比;

從觀察空間到裁減空間的變化矩陣,咱成為?Projection Matrix?。

矩陣可以用一個函數(shù)來生成:

// Generates a really hard-to-read matrix, but a normal, standard 4x4 matrix nonetheless glm::mat4 projectionMatrix = glm::perspective( ? ?glm::radians(FoV), // The vertical Field of View, in radians: the amount of "zoom". Think "camera lens". Usually between 90° (extra wide) and 30° (quite zoomed in) ? ?AspectRatio, ? ? ? // Aspect Ratio. Depends on the size of your window. such as 4/3 == 800/600 == 1280/960, sounds familiar ? ?Near, ? ? ? ? ? ? ?// Near clipping plane. Keep as big as possible, or you'll get precision issues. ? ?Far ? ? ? ? ? ? // Far clipping plane. Keep as little as possible. );

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

具體公式生成原理,這里就不展開了,詳情自行g(shù)oogle或者看這里:https://zhuanlan.zhihu.com/p/104768669。

投影矩陣雖然有投影兩個字,但是沒有做真正的投影。而是在做投影的準(zhǔn)備工作。 投影要到下一步,到屏幕空間的轉(zhuǎn)換,才使用。

進(jìn)行投影矩陣運(yùn)算后,或者說,到了裁剪空間后,w值就不一定是0和1了,有特殊的含義。具體而言,如果一個頂點(diǎn)在視椎體內(nèi),那么它變換后的坐標(biāo),必須滿足:

-w <= x <= w -w <= y <= w -w <= z <= w

  • 1

  • 2

  • 3

不符合要求的,要么剔除,要么裁減。

舉個例子:
一個人物模型的手的一個頂點(diǎn)坐標(biāo),到觀察空間的坐標(biāo)是
【9, 8.81, -27.31, 1】
經(jīng)過換算到裁減空間,值是
【11.691, 15.311, 23.69, 27.3】
那么,該頂點(diǎn)是在裁減空間內(nèi),可以被顯示。

前面都是以透視投影來說明的。那么,正交投影跟透視投影啥區(qū)別呢?
透視投影:越遠(yuǎn)的東西,呈現(xiàn)在屏幕的越??; 正交投影,遠(yuǎn)和近的同樣大小東西,呈現(xiàn)在屏幕上一樣大?。

來一張親手繪制的圖,你就知道我的意思了!

從上面可知,透視投影更有3D效果,正交透明則沒有,比較適合2D游戲。

4.3.5 屏幕空間

屏幕空間是一個二維空間了。也是我們將要看到的畫面,也是最后一個需要知道的空間了(是不是長吁一口氣,哈哈)

要把頂點(diǎn)從裁減空間轉(zhuǎn)換到屏幕空間,三維變二維了,所以所叫?投影?。

這個投影主要做兩件事:

第一,需要進(jìn)行?齊次除法?。不要覺得復(fù)雜,其實(shí)就是用w分量去處于x, y, z分量。在Open GL中,把這一步得到的坐標(biāo),叫做?歸一化的設(shè)備坐標(biāo)?(Normalized Device Coordinates, NDC)。
NDC的x, y, z的有效值范圍,都是?負(fù)1到1?。在這個值之外的,就是被剔除的點(diǎn)。

下圖是NDC的示意圖。看起來,是一個長寬高取值范圍為[-1,1]的立方體。


NDC存在意義,第二步。

第二,需要映射到屏幕,得到屏幕坐標(biāo)。在openGL中,屏幕的左下角的像素坐標(biāo)是(0, 0),右上角是(pixelWidth, pixelHeight)。
比如歸一化坐標(biāo)是(x, y),屏幕寬高是(w,h),那么,屏幕坐標(biāo)是

Sx = x*w + w/2 Sy = y*h + h/2

  • 1

  • 2

你是不是想說,NDC的z分量不要了?
不會不會, z分量也有很大的價值,它作為深度緩沖。比如,如果之后有2個頂點(diǎn),屏幕坐標(biāo)都一樣,且都不透明,那就看深度緩沖了,誰離攝像頭近,誰就顯示。

需要強(qiáng)調(diào)的是,從裁減空間到屏幕空間的轉(zhuǎn)換,是由底層幫我們完成的,不需要代碼實(shí)現(xiàn)。

頂點(diǎn)著色器,只需要把頂點(diǎn),從模型空間轉(zhuǎn)換到裁減空間即可?。

在片元著色器,可以得到該片元在屏幕空間的位置。

4.4 頂點(diǎn)坐標(biāo)變換總結(jié)

回到4.2節(jié),我們說了一個MVP矩陣,現(xiàn)在終于知道了,它代表了一個可以把頂點(diǎn)從模型空間轉(zhuǎn)換到裁減空間的矩陣!
這個矩陣,又是由?模型矩陣 * 觀察矩陣 * 投影矩陣?相乘得到的!

用一張圖來說明一下:

這和4.2節(jié)的這段代碼,終于對上了:

o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);

  • 1

這個宏?UNITY_MATRIX_MVP?,就是圖中的MVP矩陣。

當(dāng)你確定了模型位置/旋轉(zhuǎn)/縮放,就有了Model Matrix。當(dāng)確定了攝像機(jī)位置,朝向,就有了View
Matrix。當(dāng)確定了攝像機(jī)的FOV視角,近遠(yuǎn)裁切面的距離等,就有了Projection Matrix。
三者都確定了,相乘,MVP Matrix也就確定了。

所以頂點(diǎn)著色器非常的簡單,乘于一個固定MVP矩陣就OK了!然后就得到了?裁減空間?下的坐標(biāo)。
接著,GPU幫你裁減,轉(zhuǎn)換NDC坐標(biāo),屏幕映射,幾何階段完美收工??!

4.5 視頻播放的頂點(diǎn)數(shù)據(jù)回顧

咱們再往回看3.1節(jié),那是一個視頻播放的例子,頂點(diǎn)坐標(biāo)用了:

? ?private float[] vertexData = { ? ? ? ? ? ?-1f, -1f, ? ? ? ? ? ?1f, -1f, ? ? ? ? ? ?-1f, 1f, ? ? ? ? ? ?1f, 1f ? ?};

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

為什么這么簡單?因?yàn)樗鼉H用于視頻播放,畫面是矩形。只需要4個頂點(diǎn),頂點(diǎn)坐標(biāo)就是二維的,8個數(shù)字,代表4個頂點(diǎn), z值默認(rèn)為0, w值默認(rèn)為1。

如果非要自己把數(shù)據(jù)寫完整,那就是

? ?private float[] vertexData = { ? ? ? ? ? ?-1f, -1f, 0f, 1f, ? ? ? ? ? ?1f, -1f, ?0f, 1f, ? ? ? ? ? ?-1f, 1f, ?0f, 1f, ? ? ? ? ? ?1f, 1f, ?0f, 1f, ? ?};

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

所以,這里的頂點(diǎn)坐標(biāo),和裁減空間下的值是一樣的。

換句話說,視頻播放不用關(guān)心什么世界空間,觀察空間了,因?yàn)槭嵌S的,沒有這些空間,MVP矩陣就是一個單位矩陣。

另外,因?yàn)閣值為1,所以NDC下的坐標(biāo),跟裁減空間是一樣的。

4.3.5節(jié),提到了NDC坐標(biāo),是一個三維的正方體。
如果不考慮z,那么NDC坐標(biāo),是一個二維的正方形。

二維的NDC,如下圖所示:


視頻播放的頂點(diǎn)坐標(biāo),都可以用上圖作為參考。 即,頂點(diǎn)用于是4個,值,保證負(fù)一到一。

說完了頂點(diǎn),再說一下openGL紋理坐標(biāo)系,紋理只有二維的,不敢是視頻播放還是3D世界,都是二維。

坐標(biāo)系定義如下:

回到3.1節(jié),我們在加載頂點(diǎn)數(shù)據(jù)時,也包含了對應(yīng)的紋理坐標(biāo):

? ?//紋理坐標(biāo)數(shù)據(jù) ? ?private float[] fragmentData = { ? ? ? ? ? ?0f, 0f, ? ? ? ? ? ?1f, 0, ? ? ? ? ? ?0f, 1f, ? ? ? ? ? ?1f, 1f ? ?};

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

從數(shù)據(jù)可見,頂點(diǎn)坐標(biāo)和紋理坐標(biāo)有一一對應(yīng)關(guān)系,比如頂點(diǎn)坐標(biāo)左下角位置(-1,-1),對應(yīng)紋理坐標(biāo)左下角位置(0, 0)。

最后,來看一下3.1節(jié)對應(yīng)的頂點(diǎn)著色器代碼,也是簡單到令人發(fā)指:

attribute vec4 v_Position; attribute vec4 f_Position; varying vec2 textureCoordinate; void main() { ? ?gl_Position = v_Position; ? ?textureCoordinate = f_Position.xy; }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

注意,這個和4.2節(jié)不太一樣,這一段已經(jīng)是可以直接給openGL編譯的shader,4.2節(jié)是unity封裝的代碼形式,最后Unity的引擎,也會翻譯成類似上面的代碼,有main函數(shù)。

v_Position是外部輸入的頂點(diǎn)坐標(biāo),?gl_Position?是全局內(nèi)建變量,代表最后在裁減空間下的坐標(biāo)。

如果上面的shader非要使用到MVP矩陣,也是可以的,只不過,這個MVP是一個單位矩陣。乘于單位矩陣,還是一樣的值。

gl_Position = mvpMatrix * v_Position;

  • 1

5 GPU處理 ---- 光柵化階段

光柵化的主要任務(wù):計(jì)算每個圖元都覆蓋了哪些像素,然后給這些像素著色。

5.1 三角形設(shè)置與遍歷

三角形設(shè)置:

這是光柵化流水線的第一個階段。
這個階段會計(jì)算光柵化一個三角網(wǎng)格所需的信息。
具體而言,上一個階段輸出的,都是在屏幕上的二維的三角網(wǎng)格的頂點(diǎn)。即我們得到的是三角網(wǎng)格每條邊的兩個端點(diǎn)。但如果要得到整個三角網(wǎng)格對像素的覆蓋情況,我們就必須計(jì)算每條邊上的像素坐標(biāo)。為了能夠計(jì)算邊界像素的坐標(biāo)信息,我們就需要得到三角形邊界的表示方式。這樣一個計(jì)算三角網(wǎng)格表示數(shù)據(jù)的過程就叫做三角形設(shè)置。它的輸出是為了給下一個階段做準(zhǔn)備。

三角形遍歷:
三角形遍階段將會檢查?每個像素?是否被一個三角網(wǎng)格所覆蓋。如果被覆蓋的話,就會生成一個片元,而這樣一個找到哪些像素被三角網(wǎng)格覆蓋的過程就是?三角形遍歷?。三角形遍歷階段會根據(jù)上一個階段的計(jì)算結(jié)果來判斷一個三角網(wǎng)格覆蓋了哪些像素,并使用三角網(wǎng)格3個頂點(diǎn)的頂點(diǎn)信息對整個覆蓋區(qū)域的像素進(jìn)行?插值?。下圖展示了三角形遍歷階段的簡化計(jì)算過程。


根據(jù)幾何階段輸出的頂點(diǎn)信息,得到三角網(wǎng)格覆蓋的像素位置。對應(yīng)的像素會生成一個片元,片元中的狀態(tài),由三角形的頂點(diǎn)信息,進(jìn)行差值計(jì)算得到。
像上圖的三角網(wǎng)格,共產(chǎn)生了8個片元。

你一定想知道,?繪制一幀,到底會產(chǎn)生多少片元?

可以這么說,如果僅僅簡單的二維使用,比如視頻播放,那三角形就4個頂點(diǎn),鋪在了屏幕的4個角落,沒有任何深度,此時?片元數(shù)量就是屏幕的寬*高
,這應(yīng)該很容易理解,如下圖。


總共就2個三角形,深度都一樣,也就是沒有重疊的三角形,遍歷

但如果是3D游戲,就不一定了。


比如游戲中這種場景,有好幾個物理模型,模型有重疊。比如石頭后面有個房子。

繪制一幀,有多個模型需要繪制,每個模型可能都要調(diào)用一次Draw Call。一次Draw Call,可能就要產(chǎn)生一些片元。
所有物體都繪制的話,基本上所有屏幕像素坐標(biāo),都有網(wǎng)格覆蓋到,且在某些像素坐標(biāo)上,會有重疊的片元。
例如,石頭的箭頭區(qū)域像素,所對應(yīng)的三角形網(wǎng)格,跟房子的三角形網(wǎng)格,都會計(jì)算出片元,兩者的x, y一樣,但是深度z不一樣而已。
所以,?3D游戲場景,片元數(shù)量 >= 屏幕寬 * 高

5.2 片元著色器

片元著色器也是可以自定義的代碼,主要是給片元上色。
先來一段用于視頻播放渲染的著色器代碼:

varying highp vec2 textureCoordinate;// 片元對應(yīng)的紋理坐標(biāo),又頂點(diǎn)著色器的varying變量會傳遞到這里 uniform sampler2D sTexture; ? ? ? ? ?// 外部傳入的圖片紋理 即代表整張圖片的數(shù)據(jù) void main() { ? ? gl_FragColor = texture2D(sTexture, textureCoordinate); ?// 從紋理中,找到坐標(biāo)為textureCoordinate的顏色,賦值給gl_FragColor }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

是不是發(fā)現(xiàn)很簡單?!就是根據(jù)紋理坐標(biāo),采樣一下顏色,賦值給gl_FragColor就可以,作為該片元的最后顏色!

這意味著,最終顏色完全在你控制范圍內(nèi),現(xiàn)在很多視頻特效處理,就是在這個片元著色器上作文章。

舉個栗子,我們想要對一個視頻播放,看到的是黑白峰哥畫面:

#extension GL_OES_EGL_image_external : require precision mediump float; varying vec2 vCoordinate; uniform samplerExternalOES uTexture; void main() { ?vec4 color = texture2D(uTexture, vCoordinate); ?float gray = (color.r + color.g + color.b)/3.0; ?gl_FragColor = vec4(gray, gray, gray, 1.0); }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

把rgb做了一個簡單的均值,然后最后的顏色,rgb分量一樣(意味著是白色到黑色之間,gray為0黑色,gray為255,白色,gray為兩者之間,灰色),就可以得到一個黑白的顏色。然后賦值給片元,一個簡單的黑白濾鏡就完成了。

需要強(qiáng)調(diào)的是,片元著色器也是并行執(zhí)行的,有幾個片元,就執(zhí)行幾個!GPU有非常強(qiáng)大的并行計(jì)算能力。

5.3 逐片元操作

逐片元操作(PerFragment Operations)是openGL的說法,在DirectX中,成為輸出合并混合階段。

這個階段的主要任務(wù):
(1) 做模板測試,深度測試等工作,來決定每個片元的可見性!
(2)片元通過測試,把這個片元的顏色值和已經(jīng)存儲在顏色緩沖區(qū)的顏色,或者叫?混合?。

5.3.1 模板測試

這個是可選的。模板數(shù)據(jù)放在模板緩沖(Stencil Buffer)。
模板啥意思呢?很簡單,比如不管場景如何繪制,我只想看到屏幕的一個中間圓形區(qū)域,其他區(qū)域都不可見。

舉個模板例子:


片元的坐標(biāo)如果在可見區(qū)域外,則直接丟棄。

5.3.2 深度測試

如果開啟,會把該片元的深度值,即z值,和已經(jīng)存在于深度緩沖區(qū)的深度值(如果有)進(jìn)行比較。比較的方法,可以配置。默認(rèn)是,大于等于時,舍棄片元,小于時,保留該片元。這也可以理解,深度越大,說明離Camera越遠(yuǎn),被離Camera近的擋住了,所以沒必要顯示。

5.3.3 混合Blending

如果通過了前面的測試,那就要進(jìn)入混合階段了。這個也是可配置的。

為什么要合并??因?yàn)橐粠瑘D像生成,需要逐個繪制物體模型,每畫一次,就會刷新一次顏色緩沖區(qū)。當(dāng)我們執(zhí)行某次渲染時,只要不是第一次,那顏色緩沖區(qū)就有上一次渲染物體的結(jié)果?。
那么,對同樣屏幕坐標(biāo)的位置,是用上一次的顏色,還是這一次的顏色,還是混合在一起?這就是這個階段要考慮的。

對半透明物體,需要開啟混合功能,因?yàn)樾枰梢钥匆娨呀?jīng)在顏色緩沖區(qū)的顏色,所以需要混合一下,才能體現(xiàn)出透明的效果。

對不透明的物體,開發(fā)者可以關(guān)閉混合功能,畢竟,只要通過了深度測試,一般說明要保留該片元,可以直接用該片元的顏色,覆蓋掉顏色緩沖區(qū)的顏色。

5.3.4 圖像輸出-雙緩沖

首先,屏幕顯示的,就是顏色緩沖區(qū)的數(shù)據(jù)。

在繪制一幀圖像的過程中,顏色緩沖區(qū)正在不停的被覆蓋,混合中。這個過程,肯定不能讓用戶看到。所以,GPU會使用雙緩沖策略來接問題。
FrontBuffer繪制好了,交給屏幕顯示,然后把BackBuffer空閑出來,下一次的渲染,就用BackBuffer。
畫個圖就清晰了。


繪制完一幀圖像,調(diào)用一下eglSwapBuffers,就可以切換緩沖區(qū)。

6 性能討論

6.1 性能瓶頸在哪里

先說結(jié)論:?一般在CPU階段,提交命令的耗時。Draw Call數(shù)量越多,越可能造成性能問題。

在每次調(diào)用Draw
Call之前,CPU都要向GPU發(fā)送很多內(nèi)容,比如數(shù)據(jù),狀態(tài),命令等。CPU完成了準(zhǔn)備工作,提交了命令,GPU才可以開始工作。GPU渲染能力很強(qiáng),渲染100個三角形網(wǎng)格和10000個三角形網(wǎng)格,區(qū)別不大。所以,
GPU渲染速度往往優(yōu)于CPU提交命令的速度?。很多時候,都是GPU處理完了命令緩沖區(qū),空閑等待,CPU還在賣力準(zhǔn)備數(shù)據(jù)中!

這好比像,我們有很多txt文件,比如1000個,如果1000個拷貝到另一個文件夾,就很慢。如果把它打包正一個tar包,哪怕不壓縮,一次拷貝到另一個文件夾,那也比較快。
為啥呢?因?yàn)閏py本身很快,耗時主要在每次cpy之前,還要分配內(nèi)存,創(chuàng)建元數(shù)據(jù),文件上下文啥的!

如果地圖中有N個物體模型,渲染一幀完整圖像,可能就要調(diào)用N次Draw Call
,而每一次的調(diào)用,CPU都需要做很多前置工作(改變渲染狀態(tài)),那么CPU必然不堪重負(fù)。所以需要有針對性的性能優(yōu)化。

6.2 性能如何優(yōu)化

我們先來看看Draw Call對CPU的消耗大概是一個什么級別的量:
NVIDIA 在 GDC 曾提出,25K batchs/sec 會吃滿 1GHz 的 CPU,100%的使用率。

公式:

DrawCall_Num = 25K * CPU_Frame * CPU_Percentage / FPS

DrawCall_Num : DrawCall數(shù)量(最大支持)
CPU_Frame : CPU 工作頻率(GHZ單位)
CPU_Percentage:CPU 分配在drawcall這件事情上的時間率 (百分比)
FPS:希望的游戲幀率

比如說我們使用一個高通820,工作頻率在2GHz上,分配10%的CPU時間給drawcall上,并且我們要求60幀,那么一幀最多能有83個drawcall(25000
2?10%/60 = 83.33), 如果分配是20%的CPU時間,那就是大概167。

所以對drawcall的優(yōu)化,主要就是為了盡量解放CPU在調(diào)用圖形接口上的開銷。針對drawcall我們主要的思路就是?每個物體盡量減少渲染次數(shù),多個物體最好一起渲染,或者叫,批處理(batching)?。

什么樣的物體可以批處理呢?
答案是?使用同一種材質(zhì)的物體?。

同一個材質(zhì)的物理,意味著,只有頂點(diǎn)數(shù)據(jù)不一樣,但是使用的紋理,頂點(diǎn)著色器,片元著色器的代碼,都一樣!

所以我們可以把這些物體的頂點(diǎn)數(shù)據(jù)合并成一個,再發(fā)給GPU,再調(diào)用一次DrawCall。

Unity引擎支持兩種批處理方式:?靜態(tài)批處理?和?動態(tài)批處理?。其他游戲引擎的處理方式,不再這里贅述。

6.2.1 Unity靜態(tài)批處理

靜態(tài)批處理,不限制模型的頂點(diǎn)數(shù)量,它只在運(yùn)行開始階段,它只在運(yùn)動開始階段,合并一次模型。這要求這些物理不能被移動!

優(yōu)點(diǎn)?是,合并制作一次,后面渲染不用合并,節(jié)約性能。
缺點(diǎn)?是,可能占用較多內(nèi)存。比如,沒用靜態(tài)處理,?一些物體共享了網(wǎng)格
,比如地圖有100個樹木,共享一個模型。如果把這些變成靜態(tài)批處理,需要100倍的頂點(diǎn)數(shù)據(jù)內(nèi)存。

6.2.2 Unity動態(tài)批處理

動態(tài)批處理,則每一幀渲染,都需要合并一次模型網(wǎng)格。

優(yōu)點(diǎn)?是,這些物體可以被移動。
缺點(diǎn)?是,因?yàn)槊看我喜?,需要有一些限制,比如,頂點(diǎn)數(shù)目不能超過300(不同Unity版本不一樣)。

7 附錄知識

7.1 什么是齊次坐標(biāo)

齊次坐標(biāo)就是用N+1維來代表N維坐標(biāo)。
例如笛卡爾坐標(biāo)系的坐標(biāo)(x, y, z),用(x, y, z, w)來表示。坐標(biāo)可以是點(diǎn),或向量。

目的:

  1. 區(qū)分向量或者點(diǎn)。

  2. 輔助 平移T、旋轉(zhuǎn)R、縮放S這3個最常見的仿射變換

引經(jīng)據(jù)典:

齊次坐標(biāo)表示是計(jì)算機(jī)圖形學(xué)的重要手段之一,它既能夠用來明確區(qū)分向量和點(diǎn),同時也更易用于進(jìn)行仿射幾何變換。
—— F.S. Hill, JR 《計(jì)算機(jī)圖形學(xué) (OpenGL 版)》作者

關(guān)于區(qū)分向量和點(diǎn),指的是:
(1) 從普通坐標(biāo)轉(zhuǎn)換成齊次坐標(biāo)時
如果(x,y,z)是個點(diǎn),則變?yōu)?x,y,z,1);
如果(x,y,z)是個向量,則變?yōu)?x,y,z,0)

(2) 從齊次坐標(biāo)轉(zhuǎn)換成普通坐標(biāo)時
如果是(x,y,z,1),則知道它是個點(diǎn),變成(x,y,z);
如果是(x,y,z,0),則知道它是個向量,仍然變成(x,y,z)

關(guān)于平移,旋轉(zhuǎn),縮放,且慢慢道來。

7.2 平移矩陣

平移矩陣是最簡單的變換矩陣。平移矩陣是這樣的:


其中,X、Y、Z是點(diǎn)的位移增量。
例如,若想把向量(10, 10, 10, 1)沿X軸方向平移10個單位,可得:


看到?jīng)],如果平移要用矩陣來運(yùn)算,矩陣必須是4x4的,那點(diǎn)(10, 10, 10)只能增加一維w,才能被計(jì)算(矩陣乘法的要求,m?n的矩陣,只能乘于n?t
的矩陣)。

7.3 縮放矩陣

縮放也很簡單


例如把一個向量(點(diǎn)或方向皆可)沿各方向放大2倍:

7.4 旋轉(zhuǎn)矩陣

這個稍微復(fù)雜一些,沿著X, Y, Z軸的旋轉(zhuǎn)不一樣,不過最后還是一個矩陣。這里不再單獨(dú)展開。有興趣可以參考:

7.5 組合變換

上面介紹了旋轉(zhuǎn)、平移和縮放的運(yùn)算方法。把這些矩陣相乘就能將它們組合起來,例如:

TransformedVector = TranslationMatrix * RotationMatrix * ScaleMatrix *
OriginalVector;

注意,是先執(zhí)行縮放,接著旋轉(zhuǎn),最后才是平移。?這就是矩陣乘法的工作方式,也是我們對3D模型放到地圖中的常用操作方式。

3個矩陣相乘,還是一個矩陣,這就是組合變換的矩陣,所以最后也可以寫成:

TransformedVector = TRSMatrix * OriginalVector;

參考

Android OpenGL ES 1.基礎(chǔ)概念
計(jì)算機(jī)組成原理–GPU
[ 計(jì)算機(jī)那些事(8)——圖形圖像渲染原理 ](http://chuquan.me/2018/08/26/graphics-rending-
principle-gpu/)
[ opengl-tutorial ](http://www.opengl-tutorial.org/cn/beginners-
tutorials/tutorial-3-matrices/)
[ Unity文檔 ](https://docs.unity3d.com/cn/2019.4/Manual/SL-
VertexFragmentShaderExamples.html)
光柵化階段設(shè)置
關(guān)于Drawcall


GPU 渲染管線與著色器 大白話總結(jié) ---- 一篇就夠的評論 (共 條)

分享到微博請遵守國家法律
临洮县| 灵璧县| 共和县| 蒙自县| 宽城| 长治市| 洛川县| 侯马市| 兴国县| 天门市| 英德市| 自治县| 祥云县| 甘谷县| 云南省| 合作市| 汨罗市| 黎川县| 鸡东县| 肥乡县| 博乐市| 大姚县| 天等县| 霍山县| 盐津县| 梁山县| 武川县| 肥城市| 册亨县| 周宁县| 酉阳| 大理市| 泽州县| 贵州省| 金门县| 和政县| 高邑县| 宣汉县| 昔阳县| 明水县| 三门县|