目錄
(放個目錄方便預覽。知乎不支持目錄,這個目錄是從博客複製過來的,點擊會跳轉到博客)
這是《Qml特效》系列文章的特別篇,濤哥將會教大家一些ShaderEffect的基礎知識。(參考QmlBook, 譯作:著色器效果)
前面的文章,給大家展示了進場動畫,以及頁面切換動畫,大部分都使用了ShaderEffect,所以這次專門來說一下ShaderEffect。
文章主要發布在濤哥的博客 和 濤哥的知乎專欄-Qt進階之路
動畫只能控制組件的屬性整體的變化,做特效需要精確到像素。
Qml中提供了ShaderEffect這個組件,就能實現像素級別的操作。
ShaderEffect允許我們在Qml的渲染引擎SceneGraph上,利用強大的GPU進行渲染。
使用ShaderEffect,需要有一些圖形學知識,瞭解GPU渲染管線,瞭解圖形API如OpenGL、DirectX等,同時也需要一些數學知識。
圖形學的知識體系還是非常龐大的,要系統的學習,需要看很多書籍。入門級的比如「紅寶書」《OpenGL編程指南》、「藍寶書」《OpenGL超級寶典》……
一篇文章是說不完的,濤哥水平也有限。所以本文從實用的角度出發,按照濤哥自己的理解,提煉一些必要的知識點,省略一些無關的細節,
讓各位Qt開發者能瞭解GPU原理,能看懂、甚至於自己寫一些簡單的著色器代碼,就大功告成了。說的不對的地方,也歡迎大佬來指點。
先來瞭解一下,顯示器是如何顯示出各種色彩的。
假如我們把顯示器的畫面放大100倍,就會看到很多整齊排列的像素點。
繼續放大,就會發現每個像素點,由三種發光的元件組成,這三種元件分別發出紅、綠、藍三種顏色的光。三種顏色的光組合在一起,
就是人眼看到的顏色。這就是著名的RGB顏色模型。
如果把這三種光的亮度分為255個等級,就能組合出16777216種不同顏色的光。
GPU的任務,就是通過計算,給出每一個像素的紅、綠、藍 (簡稱r g b)三種顏色的數值,讓顯示器去」發出相應的光」。
(這樣說可能不太嚴謹、不太專業,只是方便大家理解。另一方面,本文的目的,
是讓大家學習如何寫特效,不是去造顯卡/造顯示器。所以請專業人士見諒!)
註:參考[1]
我們以畫一個填充色的三角形為例,來說明
下圖是一個簡易的渲染管線,引用自 LearnOpenGL
畫一個三角形,要經歷頂點著色器、圖元裝配、幾何著色器、片段著色器、光柵化等階段。
其中藍色部分是可以自定義的,自定義是指,按照圖形API規範,寫一段GPU能編譯、運行的代碼。
(這種代碼就是著色器代碼。可以自定義的這種渲染管線,就是可編程渲染管線,與之相對的是古老的固定渲染管線。)
這裡各個階段,分別引用一下,LearnOpenGL中的介紹(看不懂可以先跳過,看我畫的圖):
1 管線的第一個部分是頂點著色器(Vertex Shader),它把一個單獨的頂點作為輸入。頂點著色器主要的目的是
把3D坐標轉為另一種3D坐標(後面會解釋),同時頂點著色器允許我們對頂點屬性進行一些基本處理。
2 圖元裝配(Primitive Assembly)階段將頂點著色器輸出的所有頂點作為輸入(如果是GL_POINTS,那麼就是一個頂點),
並所有的點裝配成指定圖元的形狀;本節例子中是一個三角形。
3 圖元裝配階段的輸出會傳遞給幾何著色器(Geometry Shader)。幾何著色器把圖元形式的一系列頂點的集合作為輸入,
它可以通過產生新頂點構造出新的(或是其它的)圖元來生成其他形狀。例子中,它生成了另一個三角形。
4 幾何著色器的輸出會被傳入光柵化階段(Rasterization Stage),這裡它會把圖元映射為最終屏幕上相應的像素,
生成供片段著色器(Fragment Shader)使用的片段(Fragment)。在片段著色器運行之前會執行裁切(Clipping)。
裁切會丟棄超出你的視圖以外的所有像素,用來提升執行效率。
5 片段著色器的主要目的是計算一個像素的最終顏色,這也是所有OpenGL高級效果產生的地方。通常,片段著色器包
含3D場景的數據(比如光照、陰影、光的顏色等等),這些數據可以被用來計算最終像素的顏色。
概念還是挺多的,而且很多教程都有渲染管線圖。但是濤哥覺得,對於我們開發Shader來說,一定要有並行的意識,然而大部分
管線圖,都沒有體現出GPU的並行特性。所以濤哥自己畫了一個草圖:
解釋一下吧,CPU傳入了3個頂點到GPU,GPU將這三個頂點,傳遞給三個頂點著色器。
這裡要意識到,頂點著色器開始,就是並行處理了。GPU是很強大的SIMD架構(單指令流多數據流)。
如果我們自定義了一段頂點著色器代碼,則三個頂點著色器會同時運行這段代碼。(後面的片段著色器代碼,就是N個頂點同時運行)
頂點著色器進行處理,傳遞給圖元裝配。
圖元裝配階段,進行了頂點擴充,變成N個點,N看作三角形面積所在的點。
之後N個點依次傳給 幾何著色器->光柵化->片段著色器,最後經過測試與混合後,輸出到屏幕。
可以自定義編程的,有頂點著色器、幾何著色器、片段著色器(有的地方也叫像素著色器),順帶提一下,還有另外三種:
曲面控制著色器、曲面評估著色器 和 計算著色器。
一般我們的關注點,都會在片段著色器上。濤哥之前寫的12種特效,就只用了自定義的片段著色器。
著名的ShaderToy網站,也是隻關注片段著色器。ShaderToy
我們可以把著色器語言,當作運行在GPU上的C語言。
Qt的ShaderEffect支持的著色器語言包括OpenGL規範中的GLSL,和DirectX規範中的HLSL,這兩種著色語法上有些細微的區別,但是可以互相轉換。
我們就以glsl為主。詳細的語言規範,在khronos的官網, 各個版本都有: https://www.khronos.org/registry/OpenGL/specs/gl/
桌面版 OpenGL 版本眾多,而嵌入式系統也有專用的OpenGL ES。
安卓手機、平板設備一般就是OpenGL ES,新的設備都支持ES 3.0,老的設備一般只支持到ES 2.0
OpenGL ES 的語言規範文檔在這裡: https://www.khronos.org/registry/OpenGL/specs/es/2.0/
我們就用Qt默認的版本。
這裡用Qt幫助文檔中的示例代碼,來說明。
import QtQuick 2.0
Rectangle { width: 200; height: 100 Row { Image { id: img; sourceSize { width: 100; height: 100 } source: "qt-logo.png" } ShaderEffect { width: 100; height: 100 property variant src: img vertexShader: " uniform highp mat4 qt_Matrix; attribute highp vec4 qt_Vertex; attribute highp vec2 qt_MultiTexCoord0; varying highp vec2 coord; void main() { coord = qt_MultiTexCoord0; gl_Position = qt_Matrix * qt_Vertex; }" fragmentShader: " varying highp vec2 coord; uniform sampler2D src; uniform lowp float qt_Opacity; void main() { lowp vec4 tex = texture2D(src, coord); gl_FragColor = vec4(vec3(dot(tex.rgb, vec3(0.344, 0.5, 0.156))), tex.a) * qt_Opacity; }" } } }
這段代碼的效果是
左邊是本來的綠色的Qt的logo,右邊是處理過後的灰色logo。
ShaderEffect的vertexShader屬性就是頂點著色器了,其內容是一段字元串。按照著色器規範實現的。
同樣的,fragmentShader屬性 即片段著色器。
我們能在著色器中看到void main函數,這個便是著色器代碼的入口函數,和C語言很像。
在main之前,還有一些全局變數,我們逐條來說明一下
頂點著色器
在頂點著色器中,有這三種不同用處的變數:uniform、attribute、varying。
這些變數的值都是從CPU傳遞過來的。
如果你寫過原生OpenGL的代碼,就會知道,其中很大一部分工作,就是在處理CPU數據傳遞到GPU著色器中。
而Qml的ShaderEffect簡化了這些工作,只要寫一個property,名字、類型和著色器中的對應上,就可以了。
attribute highp vec4 qt_Vertex;
attribute是」屬性」變數,按照前面濤哥畫的管線圖來說,三個頂點著色器同時運行時,每個著色器中
的attribute值都不一樣。這裡的qt_Vertex,可以理解為分別是三角形的三個頂點。
highp是精度限定符,這裡先忽略,具體細節可以參考語言規範文檔。後面的lowp、 medium也是精度限定符。
vec4就是四維向量,類似QVector4D。
qt_Vertex是變數的名字。
這條語句的作用,就是聲明一個用來存儲頂點的attribute變數qt_Vertex。
uniform是統一變數,三個頂點著色器同時運行時,它們取得的uniform變數值是一樣的。
varying表示這個頂點著色器的輸出數據,將傳遞給後面的渲染管線。
void main() { coord = qt_MultiTexCoord0; gl_Position = qt_Matrix * qt_Vertex; }
這段main函數,將CPU傳進來的紋理坐標qt_MultiTexCoord0數據,通過varying變數coord,傳遞給了下一個階段,然後使用矩陣進行了坐標轉換,
並將結果存儲在glsl的內置變數gl_Position中。
片段著色器中,就沒有attribute了。uniform是一樣的統一變數,varying是上一個階段傳遞進來的數據。
uniform sampler2D src;
sampler2D是二維紋理。所謂紋理嘛,可以理解成一張圖片,一個Image。
src這個變數,就代表外面傳進來的那個Image。 sampler2D也可以是任意可視的Item(通過ShaderEffectSource傳遞進來)
來看一下main函數
void main() { lowp vec4 tex = texture2D(src, coord); gl_FragColor = vec4(vec3(dot(tex.rgb,vec3(0.344, 0.5, 0.156))), tex.a) * qt_Opacity; }
這裡使用了紋理
lowp vec4 tex = texture2D(src, coord);
texture2D是一個內置函數,專業術語叫「對紋理進行採樣」,什麼意思呢?
假如coord的值是(0,0),那就是對src指代的這張圖片,取x=0、y=0的坐標點的像素,作為返回值,存儲在tex變數中。
這裡注意一下紋理坐標的取值範圍。假如Qml中圖片的大小是100x100,其取值範圍從(0, 0) -> (100, 100)
這裡的傳進來的紋理坐標,取值範圍是(0, 0) -> (1, 1) ,GPU為了方便計算,都進行了歸1化處理。將範圍縮小到0 - 1
gl_FragColor = vec4(vec3(dot(tex.rgb, vec3(0.344, 0.5, 0.156) )), tex.a) * qt_Opacity;
dot(tex.rgb, vec3(0.344, 0.5, 0.156) ) 是對兩個三維向量進行了點乘。
tex.rgb是GLSL中的取值器語法。 tex是一個四維變數,可以用tex.r tex.g tex.b tex.a分別取出其中一維,也可以任意兩個組合、三個
組合取值。
rgba可以取值,xyzw也可以取值, stpq也行,但只能三種選一種,不能混用。
vec4(vec3(), tex.a) 是用三維向量再加一個變數,構造四維向量。
這條語句其實是一個RGB轉灰度的公式,原理可以自行搜索相關的資料。
gl_FragColor 是內置變數,表示所在片段著色器的最終的輸出顏色。
[1] https://zhuanlan.zhihu.com/p/43467096
[2] https://learnopengl-cn.github.io/