OpenGL ES 2.0 知識串講 (3)——Shader的功能&GLSL語法(I)

出處:電子設備中的畫家|王爍 於 2017 年 7 月 10 日發表,原文鏈接(http://geekfaner.com/shineengine/blog4_OpenGLESv2_3.html)

 

上節回顧

在第一節中,我們介紹過 OpenGL ES 與 GLSL 的主要功能,就是往繪製 buffer 上繪製圖片。其中雖然 GLSL 製作的 shader 是穿插在 OpenGL ES 中使用,但是我們在流程中可以看出來,兩大 shader(vertex shader 和 fragment shader)相對於 OpenGL ES 其他模塊還是比較獨立的。這兩個 shader 就好比兩個函數一樣,有輸入,有輸出。從 OpenGL ES 傳入一些參數,在 shader 中進行運算,然後再傳出給 GPU 的其他模塊。這一節,我們來仔細研究一下 shader 的功能。然後從這一節開始,我們將一共用四節的內容,來說一下 GLSL 的語法和如何使用 GLSL 語言 書寫 shader。


Shader 的功能

OpenGL ES 2.0 對應的 Shader 有兩種,vertex shader 和 fragment shader。

Vertex shader 的輸入爲頂點相關的信息數據,輸出分爲兩部分,必須輸出的部分是該頂點最終顯示在屏幕上的座標信息,可選輸出的是該頂點對應的其他信息(比如顏色、紋理座標等)。Vertex shader 一次只能操作一個頂點。

Fragment shader 的輸入爲屏幕上像素點的信息(座標以及座標對應像素從 Vertex Shader 傳過來的顏色、紋理座標等信息)。Fragment shader 不能修改一個像素的位置,在操作一個像素點的時候,也不能訪問旁邊的像素點。Fragment Shader 通過對頂點信息進行操作,輸出每個像素點的顏色。輸出值用於更新繪製 buffer 中的 color buffer 或者其他目標 buffer。

Shader 文件看上去其實和一個普通的.c 或者.cpp 文件很像。有預處理,會定義一些變量,有 main 函數,會根據變量進行一些運算,並得到一些結果。

在 Vertex shader 中 main 函數中最終得到的結果是頂點座標 gl_Position。

Fragment shader 結構和 VS 基本相同。在 main 函數中計算得到的最終結果是像素點的顏色值 gl_FragColor。

VS 和 PS 相似,但是又存在一些不同之處,裏面存在着一些我們從未見過的關鍵詞。從現在開始的四節內容,我們將來學習 GLSL 語法,以及如何使用 GLSL 語言編寫 Shader。


GLSL 預處理

首先我們來看預處理。

Shader 中預處理的語法與我們熟悉的語法類似。Shader 支持如下這些預處理的方式。

如果一行中只有一個#,這種用法是支持的,但是這一行會被忽略掉。#前只能放置空格或者 tab,而在其後面如果有內容的話,則是指令。

#define 和#undef 與 c/c++中類似,都是用於定義宏定義。後面選擇可以跟或不跟宏參數。

#if、#ifdef、#ifndef、#else、#elif、#endif 這些條件判斷宏與 c++中也類似, 但是稍微有一些區別,在這些宏的後面只能跟隨數字運算或者 define 定義的宏定義。未被 define 定義的識別符不會被默認爲 0,如果使用它們會導致 error。不支持字母常量。

在操作符中,不支持連字符##或者 sizeof 等。

我們也稍微舉幾個和 c++一致的例子,比如&&與操作符,只有在左邊不爲 0 的時候才計算右邊的運算。再比如||或操作符,只有在左邊爲 0 的時候,才進行右邊的運算。沒有參加運算的地方,如果使用到了一個未定義的標識符,不會報錯。

預處理中的運算是在編譯的時候進行。

#error 會將錯誤信息放到 shader 的 log 中,我們可以通過 OpenGL ES 的 API 獲取 shader 的 log。#error 後面的所有信息,一直到新的一行的開始爲止,都會 出現在 shader 的 log 中。存在#error 的 shader,我們會認爲是一個錯誤的 shader。

#pragma 是用於通過它後面的參數,控制編譯。比如它後面寫的是 STDGL, 是用於限定指令不能以 STDGL 開頭,因爲這些已經被預留了。再比如它後面可以跟 optimize(on)或者 optimize(off),這個是用於在開發和調試 shader 的時候關閉 和開啓優化使用的,必須用在 shader 的函數之外,默認情況下所有的 shader 的優化都是打開的。

pragma 後面還可以跟 debug(on)或者 debug(off),用於開啓 debug 信息。必須用在 shader 的函數之外,默認情況下所有的 shader 的 debug 都是關閉的。

由於在 GLSL 預處理的時候就會進行一些檢查,如果需要引入一些 extension, 那麼就需要在早一些被引入。在預處理中,引入 extension 是通過了#extension 來引入,引入的時候#extension 後面跟上 extension 的名字或者跟上 all,all 的意思是編譯器支持的所有 extension。然後,再在後面跟上冒號和 behavior。

當 behavior 爲 required 的時候,就是指當前 shader 需要用到某個 extension, 如果指定的 extension 不支持,返回 error。

當 behavior 爲 enable 的時候,就是指當前 shader 會打開某個 extension 的語法。比 require 的需求稍微弱一點,所以如果指定的 extension 不支持,返回 warn。

使用#extension all required 和#extension all enable 都會返回 error。

當 behavior 爲 warn 的時候,也是打開了某個 extension 的語法,如果該 extension 定義了一些 warn 的情況,那麼除非是在另外一個已經被 enable 或者 require 的 extension 支持,否則一旦遇到這些 warn 的情況,就會報 warn。如果 #extension 後面跟的是 all 的話,所有 extension 中定義的 warn 一旦遇到,就都會報 warn。如果指定的 extension 不支持,返回 warn。

當 behavior 爲 disable 的時候,指定的 extension 就不被支持。如果指定的 extension 不支持,則返回 warn。如果#extension 後面跟的是 all 的話,那麼所有 的 extension 規定的語法就都被 disable 了,僅使用 main spec 中的語法。

編譯器的初始狀態相當於執行了指令:#extension all:disable。指定當前 shader 按照 spec 規定的語法規則,並沒有引入任何 extension。

#extension 是從底層控制 extension 的方式,一般指定哪些 extension 組合需要被支持。從順序上說,後面的#extension 會覆蓋前面的#extension。而#extension all 會覆蓋前面所有的。不過只有 warn 和 disable 支持 all。

extension 的定義必須在所有非預處理語法之前,extension 的定義是有使用範圍的,如果沒有特殊指明,那麼使用範圍就是當前 shader。如果必要的話,linker可以提高 extension 的使用範圍,從而擴展到所有包含的 shader 中。

每個 extension 都有對應的宏定義,該宏被定義在 extension 的實現中。所有在 shader 中,可以使用#ifdef 判斷某個宏是否被定義了。如果被定義了,說明該宏對應的 extension 被引入了,那麼就可以在代碼中使用 extension 引入的語法, 如果沒有被定義,則不能使用該 extension 引入的新語法。

#version 用於定義該 shader 使用語言的版本號。如果使用 GLSL100,那麼這裏就要在#version 後面寫個 100。這個數值如果寫小於 100,或者大於最新的 GLSL 的版本都不對。所有的 shader 中理論上都應該存在該字段,但是在 GLSL100 中, #version 100 這個預處理並不是必須的,因爲默認 GLSL 的版本號就是 100。所以如果一個 shader 中沒有寫#version,那麼就默認爲是#version 100。

#version 必須寫在一個 shader 的最前面,前面只能有註釋或者空格。

#line 後面會跟一個常數,比如#line 10。執行了這個指令後,緊隨其後的一行代碼則被認爲是 line 10。

#line 後面也可以跟兩個常數,比如#line 10 100。執行了這個指令後,緊隨其後的一行代碼則被認爲是 line 10。而且該行的第一個 string 的 number 會被認 爲是 100。然後從該行往下,行號和 string number 都會遞加,一直到下一個#line 指令。

上述所說的這些指令中,假如在一個 shader 中使用到了兩個衝突的指令, 那麼結果就是未定義。編譯器有可能報錯,也可能不報錯,這個要看編譯器的具體實現。

在 shader 中存在一些預定義的宏定義,比如 LINE,這個宏定義代表着其當前所在行的行數+1。

再比如 FILE,代表着當前文件的文件名。

VERSION 代表當前 GLSL 的版本,常用的就是 GLSL1.0 和 1.3,對應的 VERSION 就是 100 和 130。

GL_ES 在 ES 系統中會被定義成 1,這個主要是用於判斷當前 shader 是否運 行在 ES 的系統中。

所有的以__開頭或者以 GL_開頭的宏定義都已經被保留了,也就是說開發者不可以定義這種規則的宏定義。


GLSL 註釋

Shader 的註釋語法,與我們熟知的 c、c++的語法完全一樣。都是使用/**/ 和//來指明註釋的。如果一個完整的註釋完全表達在一行中,那麼在編譯器眼中, 它就是一個空格。

Shader 中除了預處理之外,還有變量,還有 main 函數等,這些語法我們將在之後的文章中進行詳細講解。

本節教程就到此結束,希望大家繼續閱讀我之後的教程。

謝謝大家,再見!


Shader的重要性

今天和一個資深獵頭聊了會天,首先先聊聊獵頭這個行業,其實我是不反感這個行業的,甚至我覺得這個行業非常重要,獵頭和我們程序員應該是合作關係,通過獵頭,程序員可以更好的知道市場需求,在這個時刻會發生變化的時代,提高自己核心競爭力的時候,保證自己不會被市場淘汰。今天聊天的這個獵頭,就是一個我已經認識了很久,已經可以稱爲朋友的資深獵頭。

通過他我發現了一個可怕的事情,自研引擎路在哪裏,遊戲引擎可以分爲很多模塊,動作、UI、物理、地形、角色、粒子、渲染等等,而在unity如日中天的今天,昔日牛牛的自研引擎開發者還能做什麼。面臨着中年危機,所擅長的東西不再被市場認可,那麼留在原公司養老,貌似是唯一的出路了。但假如公司倒閉或者清理老員工呢。

我是從去年年初開始從自研引擎轉到研究UE4,並通過UE4做遊戲demo,在PS4上完成了一款讓索尼工作人員驚歎的demo後,由於PS4的市場佔有率問題,公司沒有下決定將這個項目做下去,然後我就轉做對Unity的項目支持,幫助公司正在使用Unity的一個項目組。

轉眼間,對項目組的Unity支持也大半年過去了,我也慢慢總結出來,自研引擎開發者在Unity上可以做什麼。幸虧我是一直堅守在圖形學領域,從手機GPU,到android app,再到遊戲引擎的渲染部分,OpenGL ES就是我的武器,那麼在unity上,我能做的一是優化,優化內存、CPU、GPU,降低遊戲的功耗;二是效果,當遊戲功耗降低到一定程度後,通過圖形學高級算法,提升遊戲的畫面質量。而圖形學高級算法的實現,大部分離不開Shader。

另外通過和獵頭的聊天,如果一個簡歷跳槽次數過多,且每家公司時間較短,是無法通過他們的背景調查的。各位看官,珍重。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章