OpenGL——對渲染的理解小結
阿新 • • 發佈:2019-01-01
最近斷斷續續,看了好久的OpenGL,終於有感覺有一點點入門了,渲染過程中,一些之前我很不理解的東西,變得有些清晰了。正如我當年研究CUDA平行計算的時候,從序列思想,到並行程式設計思想的過度,是最難的一步。
對我來說,學習的過程中遇到了一下難以馬上弄通透的問題。
難點1:不知道如何將單個例子,結合成一個完整的渲染引擎
難點2:不能把Shader和CPU的流程聯立在一起,不知道某個資源/資料,是從哪裡來的。就算知道他是什麼時候送到GPU的,也不知道GPU如何定位某個資源/資料。
難點3:難以建立起平行計算的思維方式
難點4:難以理解各種模板如何在多個批次之間發生作用。不能具象的去理解混合,測試的過程。不能夠理解,FrameBuffer的真正含義。
難點5:難以建立起空間想象,不知道矩陣變換,如何將頂點,或其他資料,變換到不同的空間進行統一計算。
難點6:難以理解從Vertex到Fragment是如何過度的。
這有點像學習騎單車,最難得無非是保持平衡,知道保持平衡之後,就能把車子騎起來了。
序列程式設計思想,到並行程式設計思想,最難理解的是,你所執行的程式碼,不是一行行的便利每個畫素或者頂點,沒有先後順序可言,這些操作是“同時”進行的,你的程式碼必須短小,儘量沒有分支,每次分支,都會大大的降低效率,所謂並行,也不是真的並行,而是GPU有很多很多粒度更小的核心,如果說CPU有4-8核的話,那麼GPU則是幾百甚至上千個“小”的核心,當然相對於CPU,他的確是非常非常的“並行”了
你的“小程式”,無論CUDA程式也好,Shader也罷,是相對並行的,在這些很多個的GPU處理器上執行的,而絕大多數情況下,你在這些並行執行的小程式之間,彼此是不知道對方的存在的。彼此之間的資料,對彼此而言,都是"未知數"
——所以你不能直接做後期處理,而是需要先把一部分資料,寫到一個FrameBuffer中區,這樣FrameBuffer裡面的資料作為共享資料,可以供後面執行的並行的小程式(也就是Shader)去獲取,這FrameBuffer裡面的資料,對於每個執行的“小程式”而言,就是可以利用的已知數了,並且通常這個FrameBuffer是隻讀使用的,你需要把計算結果寫入到另一個FrameBuffer中去,比如最終渲染的BackBuffer。
而理解渲染流水線,最難的部分在於,我總是很難理解,那麼多貼圖,那麼多模型,那麼多Shader,要怎麼在OpenGL裡面區分?這亂糟糟的一大堆東西,OpenGL怎麼才能一次畫完。
答案當然是,不可能一次畫完啦。
在引擎中,比如Unity,會在靜態合批,動態批次之後,把待渲染的資源,準備好。當然這個過程也可以是和渲染交替進行的,尤其可以用多執行緒渲染,去解除CPU對GPU的阻塞操作的等待時間,這不是重點,略過。
對於一個批次的資源,一定是公用一個material的,當然他可以有多張貼圖(並且滿足Unity其他的合批條件的,這些暫且不提)假定我們已經和好批次了,那麼接下來,引擎需要多這些東西,在一幀之內“挨個放血”。
OpenGL會先設一些大方向上的規則,比如是否使用深度測試,比如是否開啟模板測試。等等。
接下來要考慮:
我們在VAO中描述頂點是怎樣分佈的。
我們把頂點資料傳入到VBO,提交給GPU。
如果有需要,我們把EBO(頂點快取)也提交給GPU。這樣我們的頂點資料和頂點描述檔案就有了
這樣按照0,1,2,3,4的順序定義了VAO,他是和Shader裡面的描述檔案一一對應的,這樣OpenGL頂點Shader就能知道怎麼去解析VBO中的資料啦。
定義VAO:
glGenVertexArrays(1, &quadVAO);
glGenBuffers(1, &quadVBO);
glBindVertexArray(quadVAO);
glBindBuffer(GL_ARRAY_BUFFER, quadVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), &quadVertices, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 14 * sizeof(float), (void*)0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 14 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 14 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(3);
glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, 14 * sizeof(float), (void*)(8 * sizeof(float)));
glEnableVertexAttribArray(4);
glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, 14 * sizeof(float), (void*)(11 * sizeof(float)));
對應的Shader的Layout:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;
layout (location = 3) in vec3 aTangent;
layout (location = 4) in vec3 aBitangent;
我們需要為這一個批次準備裝載Shader,其中VertexShader是必須的,而FragmentShader絕大多數情況是必須的,至於幾何Shader是可選的,但是無論如何,每種Shader只能有一個,按照UnityShaderlab的規則,你可以給Shader找一個,甚至幾個備胎,但是同時並且真正生效的Shader,每種只有一個。
這也就解釋了,為什麼合批的條件是,Material必須是相同的。Unity的Materil通常會繫結一個Shader,一個Shader裡面,會包含各種Shader,頂點,幾何,片段Shader,以及他們的備胎。
如果需要繪製貼圖,那麼還需要把貼圖綁上去,依舊是以前遇到的那些問題,那麼多貼圖,我要綁哪張?OpenGL不會搞錯嗎,搞錯啦咋辦?
答案還是,這個批次用多少張貼圖,就綁多少張貼圖。至於GPU會不會快取貼圖,至於CPU提交一次貼圖要廢多大勁,那個暫時還不用我去操心。
讀取貼圖,快取和管理的過程略掉了,引擎會替我們操這個心。
像這樣
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, diffuseMap);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, normalMap);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, heightMap);
Diffuse,發現和高度貼圖就被繫結,並送到GPU了。下一個批次是不是有別的貼圖覆蓋他,那是下一個批次的事情,這次你就不用管了。
在片段程式中,如何把貼圖和這些繫結好的貼圖對相應上呢:
片段Shader中:
uniform sampler2D diffuseMap;
uniform sampler2D normalMap;
uniform sampler2D depthMap;
這樣:CPP中
shader.setInt("diffuseMap", 0);
shader.setInt("normalMap", 1);
shader.setInt("depthMap", 2);
shader是已經封裝好的,不要在意這些細節,注意 0,1,2對應GL_TEXTURE0, GL_TEXTURE1, GL_TEXTURE2,這樣“diffuseMap”,與0號Texture位,與diffuseMap這個儲存貼圖的記憶體空間,就聯立起來了。
當然還有一些其他的uniform資料要傳進去,這都很簡單了,比如光源位置,比如攝影機位置,等等。
當這些東西能夠聯立在一起時,單個批次的渲染就好理解了。
那麼批次之間有沒有什麼聯絡呢?
有的,比如你先把mask寫入模板,在下一個批次的時候,可以用它來做出一些效果,比如只顯示遮罩中的對應片段。
又如你可以將後寫入的畫素,和之前寫入的做混合,形成半透明效果。
當然你也可以讓他們沒有關聯,比如關掉對應的測試,或者清空對應的快取等等。批次之間既可以保持獨立性,又可以互相有關聯。者通過各種Buffer實現和各種開關實現。
至於先畫什麼,後畫什麼,這又是引擎(在CPU上的)的苦力活了,比如一個比較聰明的做法是先把不透明的東西畫完,然後在按照從遠到近的順序畫半透明的物體。
至於Vertex到Fragment的變換,可以詳細閱讀一下光柵化過程,尤其是相關的插值方法。
當然還有很多深層次的東西,需要我去深入學習,比如Unity引擎,他是如何做到,將這些東西,封裝成可以元件化使用的一個個元件的,Texture拖到Shader上,Shader拖到material上,Material拖到Mesh上,然後渲染Mesh的時候把這一套載入進去。並且一個場景那麼大,是如何分批,分先後去渲染的。
之前我之所以迷茫,很大程度上也是因為,我看過一個一個例子,學了一個一個技巧,就算明白了單個例子是幹什麼用的,但是無法把他們貫穿在一起,真正形成一個“渲染引擎”.
這算個非正式的感悟,等我對OpenGL在深入瞭解之後,等我下一次有頓悟的感覺的時候,我會寫個真正系統的學習總結。