學習OpenGL ES之什么是Shader?

本系列所有文章目錄

獲取示例代碼


上一篇文章中我們有說到OpenGL的渲染流程。


這其中Vertex Shader和Fragment Shader兩步是可編程的。簡而言之,Vertex Shader負責將頂點數據進一步處理,Fragment Shader將像素數據進一步處理。所以Vertex Shader中的代碼針對每個點都會調用一次,Fragment Shader中的代碼針對每個像素都會調用一次。接下來我就分三個部分講解Shader的相關知識。

如何使用Shader

要使用Shader首先要編譯Shader代碼。

bool compileShader(GLuint *shader, GLenum type, const GLchar *source) {
    GLint status;
    
    if (!source) {
        printf("Failed to load vertex shader");
        return false;
    }
    
    *shader = glCreateShader(type);
    glShaderSource(*shader, 1, &source, NULL);
    glCompileShader(*shader);
    
    GLint logLength;
    glGetShaderiv(*shader, GL_INFO_LOG_LENGTH, &logLength);
    
#if Debug
    if (logLength > 0) {
        GLchar *log = (GLchar *)malloc(logLength);
        glGetShaderInfoLog(*shader, logLength, &logLength, log);
        printf("Shader compile log:\n%s", log);
        printf("Shader: \n %s\n", source);
        free(log);
    }
#endif
    
    glGetShaderiv(*shader, GL_COMPILE_STATUS, &status);
    if (status == 0) {
        glDeleteShader(*shader);
        return false;
    }
    
    return true;
}

然后把編譯好的Shader附加到Program上,Program可以理解為一個跑在GPU上的小程序。

// Attach vertex shader to program.
glAttachShader(program, vertShader);
    
// Attach fragment shader to program.
glAttachShader(program, fragShader);

然后鏈接Program

    if (!linkProgram(program)) {
        printf("Failed to link program: %d", program);
        
        if (vertShader) {
            glDeleteShader(vertShader);
            vertShader = 0;
        }
        if (fragShader) {
            glDeleteShader(fragShader);
            fragShader = 0;
        }
        if (program) {
            glDeleteProgram(program);
            program = 0;
        }
        return false;
    }
bool linkProgram(GLuint prog) {
    GLint status;
    glLinkProgram(prog);
    
#if Debug
    GLint logLength;
    glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &logLength);
    if (logLength > 0) {
        GLchar *log = (GLchar *)malloc(logLength);
        glGetProgramInfoLog(prog, logLength, &logLength, log);
        printf("Program link log:\n%s", log);
        free(log);
    }
#endif
    
    glGetProgramiv(prog, GL_LINK_STATUS, &status);
    if (status == 0) {
        return false;
    }
    
    return true;
}

鏈接完后就可以使用了。所有和GPU交互的代碼都會用到program的值。激活Vertex Shader屬性的代碼就用到了program。

GLuint positionAttribLocation = glGetAttribLocation(self.shaderProgram, "position");
glEnableVertexAttribArray(positionAttribLocation);
GLuint colorAttribLocation = glGetAttribLocation(self.shaderProgram, "color");
glEnableVertexAttribArray(colorAttribLocation);

Vertex Shader的語法

為了介紹Shader中的uniform變量,我特地在上一篇文章的基礎上修改了Vertex Shader,如下。

attribute vec4 position;
attribute vec4 color;

uniform float elapsedTime;

varying vec4 fragColor;

void main(void) {
    fragColor = color;
    float angle = elapsedTime * 1.0;
    float xPos = position.x * cos(angle) - position.y * sin(angle);
    float yPos = position.x * sin(angle) + position.y * cos(angle);
    gl_Position = vec4(xPos, yPos, position.z, 1.0);
}

Shader的變量聲明格式為:變量類型 變量數據類型 變量名;
變量類型有三種:

  1. attribute:就是頂點數據(Vertex Data)包含的屬性,位置,顏色或是其他,頂點數據包含多少屬性,這里就可以寫多少,通過glEnableVertexAttribArrayglVertexAttribPointer激活和傳值。
  2. varying: 傳遞給Fragment Shader的變量,Fragment Shader是無法直接接受頂點數據的,因為它處理的是像素級別的數據。傳遞給Fragment Shader的值是根據像素位置插值計算之后的值。
  3. uniform: 可以理解為全局變量,所有頂點處理程序共享這個變量。

變量數據類型有:

  1. vecX: vec開頭的有vec2,vec3,vec4,分別代表二維,三維,四維向量。初始化方式分別為,vec2(x,y),vec3(x,y,z),vec4(x,y,z,w)
  2. float: 浮點數,記住,shader中int是不會自動轉換為float的,所有需要使用float的地方必須寫成浮點數格式,比如1要寫成1.0,0要寫成0.0。
  3. int: 整型
  4. matX: vec開頭的有mat2,mat3,mat4,分別代表二維,三維,四維矩陣。主要用來傳遞變換矩陣,后面使用到時會介紹。

上面的代碼的uniform變量elapsedTime表示的是程序運行經過時間的秒數。

float angle = elapsedTime * 1.0;//修改1.0為其他值可以調整轉速
float xPos = position.x * cos(angle) - position.y * sin(angle);
float yPos = position.x * sin(angle) + position.y * cos(angle);
gl_Position = vec4(xPos, yPos, position.z, 1.0);

將position圍繞(0,0,0)點旋轉角度angle,然后將旋轉后的點賦給gl_Position,就是交給OpenGL進行后續處理。angle會根據elapsedTime變化,所以點的位置也會根據elapsedTime變化。

我增加了兩行代碼來為uniform elapsedTime賦值。

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
    // 清空之前的繪制
    glClearColor(1, 0.2, 0.2, 1);
    glClear(GL_COLOR_BUFFER_BIT);
    
    // 使用fragment.glsl 和 vertex.glsl中的shader
    glUseProgram(self.shaderProgram);
    // 設置shader中的 uniform elapsedTime 的值
    GLuint elapsedTimeUniformLocation = glGetUniformLocation(self.shaderProgram, "elapsedTime");
    glUniform1f(elapsedTimeUniformLocation, (GLfloat)self.elapsedTime);
    
    [self drawTriangle];
}

先獲取uniform elapsedTime的位置,因為elapsedTimefloat類型,所以使用glUniform1f進行賦值。如果是其他類型的uniform,就需要使用其他的glUniformXXX方法賦值了。后面用到的時候再詳細介紹。

這個Shader具體的效果如下:

shader.gif

每個頂點都會旋轉,所以最后整個三角形都在旋轉。如果你樂意的話,還可以在Vertex Shader中對頂點位置進行縮放或者移動。試試看會有什么樣的效果。

Fragment Shader的語法

varying lowp vec4 fragColor;
uniform highp float elapsedTime;

void main(void) {
    highp float processedElapsedTime = elapsedTime;
    highp float intensity = (sin(processedElapsedTime) + 1.0) / 2.0;
    gl_FragColor = fragColor * intensity;
}

Fragment Shader的變量只能是uniformvarying,這里的varying是從Vertex Shader傳過來的值。與Vertex Shader不同的是,這里的變量都要聲明精度,比如highp float processedElapsedTime = elapsedTime;中的highp代表高精度。精度包括lowp highp mediump,低精度,高精度,中等精度。如果你不想為每一個變量都指定精度可以在第一行寫上precision highp float;,當然這只是為float指定默認精度highp。要為其他類型指定精度的話繼續加就好了。比如再加上precision highp vec2;
上面的Fragment Shader中我把傳遞過來的顏色根據當前的elapsedTime進行了計算,顏色的強度會隨著(sin(processedElapsedTime) + 1.0) / 2.0;的曲線變化。這里我做了vec4 * float的運算,結果仍然是vec4vec4(a,b,c,d) * f的運算結果是vec4(a*f,b*f,c*f,d*f)
使用了這個Fragment Shader后效果如下。

本文主要通過Shader的兩個小動畫介紹了Vertex Shader和Fragment Shader的基本語法和功能。如果你想要了解更多Shader中的運算規則和內置函數請參見:
OpenGL-ES-2_0-Reference-card
我還發現了一個比較有意思的網站,可以直接在線編輯預覽Fragment Shader。
http://haxiomic.github.io/webgl-workshop/editor//index.html

time是一個默認的uniform,會隨著時間改變。uv是紋理坐標,有兩個值s和t,值從0到1。有興趣的可以去玩玩。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374

推薦閱讀更多精彩內容