目錄

(放個目錄方便預覽。知乎不支持目錄,這個目錄是從博客複製過來的,點擊會跳轉到博客)

  • 簡介
  • 關於文章
  • ShaderEffect
  • 顯示器如何顯示色彩
  • GPU渲染流程
    • 渲染管線圖
    • 並行管線示意圖
  • 著色器語言編碼規範
  • 著色器代碼示例
    • 示例
    • 著色器代碼
    • 頂點著色器
    • 片段著色器
  • 參考文獻

簡介

這是《Qml特效》系列文章的特別篇,濤哥將會教大家一些ShaderEffect的基礎知識。(參考QmlBook, 譯作:著色器效果)

前面的文章,給大家展示了進場動畫,以及頁面切換動畫,大部分都使用了ShaderEffect,所以這次專門來說一下ShaderEffect。

關於文章

文章主要發布在濤哥的博客 和 濤哥的知乎專欄-Qt進階之路

ShaderEffect

動畫只能控制組件的屬性整體的變化,做特效需要精確到像素。

Qml中提供了ShaderEffect這個組件,就能實現像素級別的操作。

ShaderEffect允許我們在Qml的渲染引擎SceneGraph上,利用強大的GPU進行渲染。

使用ShaderEffect,需要有一些圖形學知識,瞭解GPU渲染管線,瞭解圖形API如OpenGL、DirectX等,同時也需要一些數學知識。

圖形學的知識體系還是非常龐大的,要系統的學習,需要看很多書籍。入門級的比如「紅寶書」《OpenGL編程指南》、「藍寶書」《OpenGL超級寶典》……

一篇文章是說不完的,濤哥水平也有限。所以本文從實用的角度出發,按照濤哥自己的理解,提煉一些必要的知識點,省略一些無關的細節,

讓各位Qt開發者能瞭解GPU原理,能看懂、甚至於自己寫一些簡單的著色器代碼,就大功告成了。說的不對的地方,也歡迎大佬來指點。

顯示器如何顯示色彩

先來瞭解一下,顯示器是如何顯示出各種色彩的。

假如我們把顯示器的畫面放大100倍,就會看到很多整齊排列的像素點。

繼續放大,就會發現每個像素點,由三種發光的元件組成,這三種元件分別發出紅、綠、藍三種顏色的光。三種顏色的光組合在一起,

就是人眼看到的顏色。這就是著名的RGB顏色模型。

如果把這三種光的亮度分為255個等級,就能組合出16777216種不同顏色的光。

GPU的任務,就是通過計算,給出每一個像素的紅、綠、藍 (簡稱r g b)三種顏色的數值,讓顯示器去」發出相應的光」。

(這樣說可能不太嚴謹、不太專業,只是方便大家理解。另一方面,本文的目的,

是讓大家學習如何寫特效,不是去造顯卡/造顯示器。所以請專業人士見諒!)

註:參考[1]

GPU渲染流程

我們以畫一個填充色的三角形為例,來說明

渲染管線圖

下圖是一個簡易的渲染管線,引用自 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的官網, 各個版本都有: khronos.org/registry/Op

桌面版 OpenGL 版本眾多,而嵌入式系統也有專用的OpenGL ES。

安卓手機、平板設備一般就是OpenGL ES,新的設備都支持ES 3.0,老的設備一般只支持到ES 2.0

OpenGL ES 的語言規範文檔在這裡: khronos.org/registry/Op

我們就用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] zhuanlan.zhihu.com/p/43

[2] learnopengl-cn.github.io


推薦閱讀:
相關文章