GLSL vary、atrribute、in、out的区别
這個問題困擾我很久了,前一段時間面試,面試官問我GLSL中的vary、atrribute,當時我就懵逼了,一陣心虛,我只說我知道in out,應為我學的就只有in out,回來自己也查了查,GLSL中確實有vary、atrribute這么用的,但是一直沒搞清楚他們與in、out的區別聯系,今天看了這篇文章豁然開朗,原來是vary、attribute是老版本的GLSL中的關鍵字,現在已經統一使用in、out了,當時特么白心虛了,是面試官知識結構太老,當時我說in、out,他貌似不知道。。。廢話不多說了,直接上原文。
OpenGL/GLSL規范在不斷演進著,我們漸漸走進可編程管道的時代的同時,嶄新的功能接口也讓我們有點繚亂的感覺。本文再次從OpenGL和GLSL之間數據的傳遞這一點,記錄和介紹基于OpenGL3.x的新方式,也會適時介紹Unform Buffer Objecct(UBO)這一重要特性。——ZwqXin.com
本文可視為大致一年半前的本博客的[OpenGL/GLSL數據傳遞小記(2.x)]一文的延續。對這方面不熟悉的話請先瀏覽一下該文中介紹的基本概念。在該文中,我把這些傳遞分為attribute變量、uniform變量、varying變量和Fragment Shader輸出,這四部分(主要講述前兩部分)。而本文再次按此四部分,談談在GL3.x(NVidia 8Series以后顯卡所支持的OpenGL版本)中的數據傳遞方式的變化。
本文來源于?ZwqXin?(http://www.zwqxin.com/), 轉載請注明
??????原文地址:http://www.zwqxin.com/archives/shaderglsl/communication-between-opengl-glsl-2.html
1. attribute變量
在前文中提及到GLSL中每一個attribute變量都有一個“位置”值(Location),在ShaderProgram鏈接(link)前,可以Bind之,鏈接之后,可以Get之。通過這兩種方式都可以建立attribute變量與頂點屬性的聯系。如今引入第三種方式——直接在GLSL代碼中指定這些位置值:
glsl3.x代碼 (Vertex Program)在上面的Vertex Program代碼中,第一行(#version?330),表明我們現在使用的GLSL版本是GLSL3.3,以區別于以前的版本并允許我們使用基于GLSL3.3的功能。在過去,OpenGL的版本和GLSL版本是不統一的(前文中的GL2.2所對應的是GLSL1.2,而后來的對應關系是GL3.0-GLSL1.3,GL3.1-GLSL1.4,GL3.2-GLSL1.5),直到2010年OpenGL3.3/4.0規范的提出,khronos委員會決定讓兩者版本統一,所以就有了現在本博客所使用的OpenGL3.3-GLSL3.3的對應關系(注,ShaderModel4.0的顯卡可達到的最高版本)。
接下來的幾行聲明了4個attribute變量。在GL2.x中一個attribute變量通常是“attribute vec3?attrib_position;”這樣來表示,在GL3.x中,廢棄了attribute關鍵字(以及varying關鍵字),屬性變量統一用in/out作為前置關鍵字,對每一個Shader stage來說,in表示該屬性是作為輸入的屬性,out表示該屬性是用于輸出的屬性。這里,attribute變量作為Vertex Shader的頂點輸入屬性,所以都用in標記。另外,這里使用了layout關鍵字(通常是layout(layoutAttrib1=XXX, layoutAttrib2=XXX, ...)這樣的形式)。這個關鍵字用于一個具體變量前,用于顯式標明該變量的一些布局屬性,這里就是顯式設定了該attribute變量的位置值(location),其作用跟ShaderProgram(著色程序)鏈接前調用glBindAttribLocation來設定atribute變量的位置值是等效的。
為什么采用這種方式更好呢?其一當然是編碼量減少了,二來也避免了去Get某個attribute的location帶來的開銷,三來,最重要的是,它重定義了OpenGL和GLSL之間attribute變量屬性的依賴。過去我們的OpenGL端必須首先要知道GLSL端某個attribute的名字,才能設置/獲得其位置值,如今兩者只需要location對應起來就可以完成繪制時頂點屬性流的傳遞了。不再需要在ShaderProgram的compile和link之間插入代碼也更方便于其模塊化。
2.uniform變量
對于uniform變量的聲明方式,跟GL2.x的一致,使用uniform關鍵字就可以了。
glsl代碼每一個uniform變量也都有其一個“位置值”(Location),在OpenGL中,我們可以通過glGetUniformLocation來獲得。那么我們可以不可以像attribute變量那樣,在Shader代碼中顯式指定這個Location呢?(其好處也是跟上述差不多的,但就是如果uniform變量太多的話這樣做也麻煩,因為得在代碼中一個一個指定不重復的location。)嘛,attribute變量location的顯式指定,是經由GL擴展GL_ARB_explicit_attrib_location實現的,而事實上,現在也有GL_ARB_explicit_uniform_location這樣一個GL擴展,能實現這樣的功能,只不過它是OpenGL4.3標準的一部分,隸屬于GLSL4.3,所以即使GL3.x支持這個擴展,我們還是暫時不要用的好。
那我們就像往常一樣,在glUseProgram啟用了某個ShaderProgram之后,一個一個地給每個unifom變量關聯數據咯(通過其location)——等等,這是在運行期間設置數據值吧,那如果我這個關聯數據并不是每幀都變化的,甚至它是一個固定值,這樣做豈不太無聊太浪費了?事實上我們還是可以在glUseProgram之外綁定數據的——乃至直接在初始化時。這得益于glProgramUniform系列函數的引入,它比起往常的glUniform要多一個參數用來接收一個ShaderProgram的ID。在建立ShaderProgram后,我們也不需要glUseProgram來預先綁定它就可以直接取得某個uniform變量的location值并用glProgramUniform系列函數關聯數據,而且這個數據在其后運行期間的每次glUseProgram后都不會失效。從理論上將,這族函數完全可以替代glUniform系列函數(是它們功能的一個超集),但是就不知道會不會有性能上的損失了(這個暫時目前找不到說法),所以我暫時建議是只對那些非動態變化的uniform變量使用了。
再來看看uniform變量的問題。通常一個稍微復雜點點、更多控制參數的Shader,都會有大量的Uniform變量需要設置,所以導致了我們很多時候在glUseProgram之后要調用一長串的glUniform函數來傳遞該Pass的數據。有沒有方法盡量把這些操作合并呢?另外,我們知道一個Shader的可用Uniform數據大小是有一個上限值的(例如我目前顯卡的一個vertex shader的GL_MAX_VERTEX_UNIFORM_COMPONENTS值是4096,意味著我在一個VertexShader里使用的active uniforms,大概就是最多4096個float/int值了,或者說最多1024個vec4、最多256個mat16),那么有沒辦法提高這個上限呢?在[MD5模型的格式、導入與頂點蒙皮式骨骼動畫II]這篇文章中,因為擔心uniform數量不足以支撐傳入的眾多個骨骼矩陣,所以優先選擇TBO(Texture Buffer Object)作為傳入數據的媒介,把數據裝入一個一維紋理的Buffer中以提供給Shader。那么除了使用紋理數據外,還有沒有更直接的方式呢?
?Uniform Buffer Object(UBO)
UBO,顧名思義,就是一個裝載Uniform變量數據的Buffer Object。就概念而言,它跟VBO([學一學,VBO] )之類Buffer Object差不多,反正就是顯存中一塊用于儲存特定數據的區域了。在OpenGL端,它的創建、更新、銷毀的方式都與其他Buffer Object沒什么區別,我們只不過把一個或多個uniform數據交給它,以替代glUniform的方式傳遞數據而已。這里必須明確一點,這些數據是給到這個UBO,存儲于這個UBO上,而不再是交給ShaderProgram,所以它們不會占用這個ShaderProgram自身的uniform存儲空間,所以UBO是一種全新的傳遞數據的方式,從路徑到目的地,都跟傳統uniform變量的方式不一樣。自然,對于這樣的數據,在Shader中不能再使用上面代碼中的方式來指涉了。隨著UBO的引入,GLSL也引入了uniform block這種指涉工具。
glsl代碼uniform block是Interface block的一種,(layout意義容后再述)在unifom關鍵字后直接跟隨一個block name和大括號,里面是一個或多個uniform變量。一個uniform block可以指涉一個UBO的數據——我們要把block里的uniform變量與OpenGL里的數據建立關聯。 因為這些uniform變量不是存儲在Shader的“uniform區域”里的,所以也就沒有那一套“位置值”(location),那么我們通過什么建立關聯呢?
對于每一個uniform block,都有一個“索引值”(index),這個索引值我們可以在OpenGL中獲得,并把它與一個具體的UBO關聯起來。這樣block內的數據聲明就會與UBO中的實質數據聯系起來了:
OpenGL代碼一般我們可以使用glGetUniformBlockIndex來獲取這個Index,但擴展GL_ARB_program_interface_query引入了比較統一的獲取ShaderProgram內資源的相關屬性的API(詳見此擴展的spec),所以也可以以GL_UNIFORM_BLOCK調用glGetProgramResourceIndex來獲取資源的Index。得到名為matVP的uniform block的Index后,我們可以查詢這個block的相關信息(glGetActiveUniformBlockiv)。為了建立合適大小的UBO,這里查詢了這個block所需的字節大小(GL_UNIFORM_BLOCK_DATA_SIZE)的值(注意這個值代表此block所占的大小,它可能會比block內數據實際相加后的值要大,下面會再述)。
建立一個UBO的過程跟建立其他類型的Buffer Object相似,不過Target是GL_UNIFORM_BUFFER,數據為空。接下來是把一個UBO(ID為m_nUBO)和Shader內的uniform block(Index為nMatVPBlockIndex)相關聯:把它們都關聯到同一個uniform buffer binding-point。其中前者通過glBindBufferBase或glBindBufferRange來完成,其中第二個參數就是binding-point,這里選擇的是binding-point_0(參數值為0,當然你可以輸入1、2、3...以選擇binding-point_1、binding-point_2、binding-point_3…);同樣,對于后者uniform block,也通過glUniformBlockBinding來完成,其中第三個參數是binding-point,這里同樣選擇了第0個binding-point——這樣OpenGL端的UBO和GLSL端的uniform block就聯系在一起了。Shader中需要使用block中的uniform變量時,就會索引到對應的UBO中對應的位置的數據。
所謂binding-point(或者說binding-location),我理解為是OpenGL的Context上的一個個狀態位。通常來說,我們可以建立非常多的UBO,它們的數據區在顯存中,以ID標識,一般通過Context綁定一個UBO的ID的方式讓OpenGL去尋找對應的顯存位置——這是一種非常耗時的操作(應該說,所有bind類的操作都是)。數據需要更新就算了,但如果Shader執行時也必須為每個uniform block去綁定、尋覓數據區……為避免這樣的情況所以就需要一個足以減少消耗的橋梁物,這個中間物件保存著能夠直達具體某個UBO數據區的“方式”(不妨暫假想為該數據區的起始顯存地址、長度等),然后我們把這個中間物件的位置告訴Shader,讓Shader在需要時直接“來到”這個中間件中獲取某個顯存區的實質數據。這里與前者最大的區別應該就是Shader到中間件的用時——這應該足夠快。所以首先這個中間物件應該存儲在OpenGL的Context上(于是它名義上就是一個OpenGL狀態),OpenGL內的對象的交流是比較便捷的,至少比Bind方式去存取“遙遠的”顯存數據要快不少,其次這個中間物件自身也應該容易表示,讓Shader能“直接認門牌”——這些中間物件就是單純Zero-Base數字序列形式的uniform binding-point,OpenGL通過它一步定位到實質數據處。
OpenGL Context本身也應該是一個盡量小體積的東西,所以不便在它身上放太多這種binding-point。在我的顯卡上,GL_MAX_UNIFORM_BUFFER_BINDINGS的個數為36,這表示同一時間能映射的UBO-uniform block關系最多只有36對(間接也限制了一個ShaderProgram中uniform block的個數),哪怕你有大量的UBO,為了以上機制的實行,也只能接受這個限制。我們就是通過glBindBufferBase/glBindBufferRange來我UBO或UBO中的某分區的信息存儲至某個binding-point上,然后通過glUniformBlockBinding來“通知”ShaderProgram某個uniform block的數據信息存儲在哪個binding-point上。如果把glUniformBlockBinding當成glUniform族函數,這個操作會更親切一點:只不過如今對于目標block使用的是Index而不是Location(事實上它的行為更類似上面提到的glProgramUniform族函數,因為不需要事先glUseProgram啟用某個ShaderProgram而是作為首參罷)。
除了UBO,前面某篇博文[亂彈紀錄IV:Transform Feedback]中提到的Transform Feedback Buffer也是使用binding-point(參見文中代碼段)的“好手”。因為Shader同樣需要快速找出需要feedback的那個Buffer的所在地,尤其是通過GL_SEPARATE_ATTRIBS的方式為每一個輸出數據獨立指定buffer時,就需要用到多個transform-feedback binding-point來儲存各個buffer的信息了。其限制個數其實就是GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS了(本顯卡此數字為4)。
這里引出一個問題:我們能不能像TransformFeedback那樣,為一個UBO對象非配數個binding-point呢?可以的。這樣做的目的也很明確——單個UBO多個uniform-block。準確地說,是每個uniform block對應該UBO存儲區域中不同的分區域(sub-region)——glBindBufferRange,就是你了!
OpenGL代碼上面代碼段中,我們把兩個uniform-block關聯到同一個UBO的兩個區域:[0 ~?nBlockDataSize1]、[nUniformBufferAlignSize ~?nUniformBufferAlignSize+nBlockDataSize2]。為什么第二個block不是映射到[nBlockDataSize1 ~?nBlockDataSize1+nBlockDataSize2]呢?這里有個比較重要的概念:數據對齊。對于uniform-block,可以通過GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT找出這個對齊值(本顯卡上此數值是256字節,所以每個uniform block都是256字節對齊,相鄰uniform block的間隔必須滿足256字節的整數倍,否則會發現數據會對不上)。記住了,block與block的數據不是緊緊pack在一起的。很容易想象,跟CPU上存儲結構體一樣,這是為了數據存取效率考慮,至于為什么是這個值,就要更深入研究了。
麻煩可不止這一個——單個uniform-block里面的數據,也有字節對齊的機制——這給uniorm變量的數據更新帶來更大的麻煩。
先來大致了解一下上面的GLSL代碼的uniform block前面的layout內容。一般uniform block按數據組織類型可分為三種(目前):packed、shared、std140,我們可以在它們前面用layout去指定該block屬于哪種類型(也可以全局設置,也就是把layout單獨作為一語句,此時它影響隨后的各個沒前接layout的uniform block)。
UBO的一個最顯眼的好處就是實現數據共享。譬如我上面的matPV這個uniform block就是最好的例子:通常渲染場景時,只會有一個視圖矩陣和一個投影矩陣([亂彈OpenGL中的矩陣變換(上)] [亂彈OpenGL中的矩陣變換(下)] ),而且它們相對每一幀都是固定數據。而我們可能場景里物件用到的Shader不一樣,但它們都得通過這兩個矩陣計算最終的頂點輸出啊?以前的話,我得每個Shader都傳一次這些相同的矩陣數據,不僅時間上glUniform族函數會比較多而且空間上也分別占每個ShaderProgram本身的同等的存儲資源。如今把它們統一在一個UBO中,每幀更新就只要更新UBO一次就可以了,而且也只占一份的資源空間(在顯存上)。
為什么突然插播以上“廣告”呢?因為這對數據組織形式影響甚大。為了實現數據共享,必須保證各個shader里的指涉該UBO的unifom block“一模一樣”。但我們也知道([OpenGL/GLSL數據傳遞小記(2.x)] ),GLSL編譯器會檢查并自動刪掉那些非active(在shader中沒有實質用途)的uniform變量。那么,假如我們的多個Shader里都有相同的uniform block,而里面某個變量x被ShaderA用到而沒背ShaderB用到,那么前者就會把它默默刪掉,這樣數據結構不統一,自然映射到同一個UBO也無法預計得到各個子數據的具體位置(必須得針對每個Shader的uniform block內每個變量查詢它的Offset)——block內的這種“檢查”機制由packed這種layout掌控,為了要關閉這種機制,就需要選擇其他三種layout。而shared(順帶一提,這個是默認layout)與std140不同之處在于,它雖然不會“刪掉”block內的non-active變量,而且保證這些uniform block內的數據在存儲分布上的一致性(所以各個shader能共享同一個block結構),但它不會去固定統一存儲分布,所以還是有必要去查詢各個變量的offset(因為可能在顯卡A上這個offset是16在顯卡B上就變32了)。至于std140等,其實就是排除這些因素而有著嚴格限制的一個數據組織結構“無優化”的版本,所以一般的場合下我們應該首選這類std(OpenGL-Standard)的layout。順帶說一下因為最初UBO/uniform block是跟隨OpenGL3.1/GLSL1.4引入的所以有此std140之名(其實現存的類似layout還有個std430,但它是專門留給OpenGL/GLSL4.3的storage buffer block產生更小的offset而用的,按此不表)。
說了那么多,既然一般應用首選std140,那么它那個固定的offset是多少呢?根據我的不嚴格查驗(沒驗證多個顯卡),其值為16字節,也就是說數據按16字節對齊。而數據中還再分為vector、數組、矩陣這些,也是按類似規則限制(不一一舉出,查spec去吧)。舉例一下吧:
glsl代碼要更新這兩個block對應的那個UBO,應該這樣:
C++代碼其實UBO除了能夠共享unifom變量數據外,上面的敘述還隱含有它的兩個重要優點:一點是索引、切換binding-point的速度比較快,比起多個glUniform的調用傳遞數據也更快;另一點是對于顯存中的uniform數據,可用的存儲空間也大幅增加(而且對比TBO,UBO更適合需要線性存取的數據)——這回應了介紹UBO前的那兩個問題。
UBO的介紹暫到此為止,真的很費口水——因為我覺得它本身就是包含不少要點的OpenGL功能——其實要點還不止這些,也還是需要更進一層地了解才行。uniform數據應盡量使用UBO來存放,尤其是那些需要Shader共享的數據,當然了零碎細小的數據還是glUniform/glProgramUniform類函數會更方便點吧~
3.varying變量
?varying變量主要用于在Shader Stage間進行傳遞,注意的是在光柵化(Rasterization)的時候,這些變量也會跟著一起被光柵插值。那如果我們不想某個頂點屬性被光柵化,該怎么辦呢?在[OpenGL常用命令備忘錄(Part A)]這篇文章提到的一個古老API,glShadeModel,它在固定管道渲染流水線上能起到控制圖元屬性是否被插值的功效(需要光柵化時傳入參數GL_SMOOTH,不需要時傳入GL_FLAT),那么當選擇不插值時(GL_FLAT),流水線上發生了什么呢?
假設現在流水線上,經過裁剪、歸一化等,生成了一個屏幕上的三角圖元(三個頂點上的顏色屬性分別是c1、c2、c3),進入光柵化階段。假如進行插值,三角圖元里各像(假設共n個)素會根據其各自位置對三個頂點的顏色值進行線性插值,生成對應的n個顏色值(cList[n]);假如不插值,則該三角形里所有像素都會是同一個值(cConst),這個值可能等于c1、c2或c3其中一個。到底是哪一個呢?這取決于哪個頂點是provoking-vertex(在[亂彈紀錄I:Geometry Shader] 中也提及過它)。你可以在OpenGL端通過glProvokingVertex函數改變這個設置(參數GL_FIRST_VERTEX_CONVENTION/GL_LAST_VERTEX_CONVENTION決定取圖元繪制順序的第一個頂點還是最后一個頂點作為provoking-vertex)。
其實要讓GLSL中某個作為頂點屬性的varying變量不被光柵化,只要在它前面加一個flat關鍵字就可以了。這樣它就像上述的那樣,到達Fragmen Shader的圖元上所有像素的該varying值都是相同的值(provoking vertex上的值):
glsl代碼同樣在[亂彈紀錄I:Geometry Shader]中也提到這樣一個問題:一個ShaderProgram中不能有兩個同為輸入的同名varing變量,也不能有兩個同為輸出的同名varing變量存在。所以即使表示的是同一個變量,也得使其名字不一樣:
glsl代碼這樣的話,在有些場合需要實現不同shader的組合——譬如實現一個可加入也可不加入的Geometry Shader,就難辦了(何況當代流水線上的Shader可不止這三個呢)。為解決這個麻煩,也為了把變量聲明組織得更“好看”一些,我們再次用到interface block。上面的uniform block是其中一種,但它還包括in block和out block這兩種可用于varing變量的:
C++代碼注意這里使用了block insatnce name(緊隨大括號后的那個名字),這個名字對各Shader Stage來說都是獨特的,所以改成上面這樣的話,block之間也不會發生名字沖突,block內的varying變量也就可以用同一個名字了。使用時需要按"blockInstanceName.varyingVariable"的類似結構體內變量的樣式來表示:
glsl代碼block自帶組織多個變量聲明的功效:
glsl代碼另外,對于Transform Feedback([亂彈紀錄IV:Transform Feedback] ),指定輸出Varing屬性時,也要按上述的結構體內變量表示法:
C++代碼4.fragmentShader輸出
最后,再簡單談一下fragmentShader的輸出。一般來說,輸出的是顏色值,輸出目標是Frame Buffer。這又包括常規的輸出到屏幕Buffer、輸出到FBO([學一學,FBO] ),另外還可以通過MRT(Multi Render Target)輸出到兩個以上的FBO中。但是,這些對于Fragment Shader來說并沒太多不一樣:通過ShaderProgram鏈接前的glBindFragDataLocation指定輸出到第幾個Buffer(默認是0)。類似于上述的attribute變量,我們也可以直接通過layout來指定這個location值:
glsl代碼 (fragemnt shader)然后只要在FragmentShader中把結果對應地賦給這些輸出型變量就可以了。但是,這些layout里的關鍵字其實還有個index——只是默認為0而已:
glsl代碼 (fragemnt shader)它們同樣是輸出到第0個緩沖區,但是其中有一個的index為1——這個src1Color是所謂的Second Output。它同樣儲存在一塊緩沖區域中,但我們在OpenGL中怎么獲得這個區域的顏色值呢?答案就是由GL_ARB_blend_func_extended擴展引入的,新的混合參數(GL_SRC1_COLOR/GL_SRC1_ALPHA等等這類新舊的enum)。它們作為混合因子而存在——這里輸出的src1Color,就只能作為各個對應像素混合因子來用。簡單舉例:
C++代碼該代碼啟用混合,當前繪制的內容(混合源src,即fragColor)的混合因子是自己的alpha值,而背景(混合目標dst,即繪制前此FrameBuffer的內容)處對應的被覆蓋像素的混合因子則是該對應像素輸出的src1Color值,其中RGBA分量分別用于混合RGBA四個通道:
finalColor = sourceColor * sourceAlpha + destinationColor * src1Color
?
好了,本文于此結束。如有批誤或疏忽提醒,請大牛們不膩賜教或指出給ZwqXin,謝謝。
本文來源于?ZwqXin?(http://www.zwqxin.com/), 轉載請注明
??????原文地址:http://www.zwqxin.com/archives/shaderglsl/communication-between-opengl-glsl-2.html
總結
以上是生活随笔為你收集整理的GLSL vary、atrribute、in、out的区别的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用Adaptive cards来构建T
- 下一篇: 关于idea maven ojdbc6.