獲取示例代碼
上一篇文章中我們有說到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的變量聲明格式為:變量類型 變量數據類型 變量名;
變量類型
有三種:
-
attribute
:就是頂點數據(Vertex Data)包含的屬性,位置,顏色或是其他,頂點數據包含多少屬性,這里就可以寫多少,通過glEnableVertexAttribArray
和glVertexAttribPointer
激活和傳值。 -
varying
: 傳遞給Fragment Shader的變量,Fragment Shader是無法直接接受頂點數據的,因為它處理的是像素級別的數據。傳遞給Fragment Shader的值是根據像素位置插值計算之后的值。 -
uniform
: 可以理解為全局變量,所有頂點處理程序共享這個變量。
變量數據類型
有:
-
vecX
: vec開頭的有vec2
,vec3
,vec4
,分別代表二維,三維,四維向量。初始化方式分別為,vec2(x,y)
,vec3(x,y,z)
,vec4(x,y,z,w)
-
float
: 浮點數,記住,shader中int是不會自動轉換為float的,所有需要使用float的地方必須寫成浮點數格式,比如1要寫成1.0,0要寫成0.0。 -
int
: 整型 -
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
的位置,因為elapsedTime
是float
類型,所以使用glUniform1f
進行賦值。如果是其他類型的uniform
,就需要使用其他的glUniformXXX
方法賦值了。后面用到的時候再詳細介紹。
這個Shader具體的效果如下:
每個頂點都會旋轉,所以最后整個三角形都在旋轉。如果你樂意的話,還可以在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的變量只能是uniform
和varying
,這里的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
的運算,結果仍然是vec4
。vec4(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。有興趣的可以去玩玩。