前言
前面的基礎(chǔ)文章列表
寫在前面:OpenGLES系列是自己在做音視頻時(shí)候邊學(xué)邊記錄的文章 距離最初記錄已經(jīng)三年有余,現(xiàn)在回頭看補(bǔ)齊這個(gè)當(dāng)初的學(xué)習(xí)系列也算是一種回溯吧,學(xué)習(xí)總是讓人開心的,由陌生到熟練再到更進(jìn)一步精通的過(guò)程充滿著樂(lè)趣。希望自己抽出時(shí)間盡快將這個(gè)基礎(chǔ)系列補(bǔ)齊。如今雖然OpenGL在iOS上已經(jīng)被蘋果廢棄了,不過(guò)圖形框架的基礎(chǔ)原理和思路還是很相像的,熟悉opengl再熟悉metal會(huì)更快上手
初識(shí)GLSL
之前的章節(jié)提到過(guò) 我們使用GLKit框架所以避開了寫繁瑣的GLSL
GLSL是一個(gè)以C語(yǔ)言為基礎(chǔ)的高階著色語(yǔ)言 通過(guò)編寫glsl 提供開發(fā)者對(duì)繪圖管線更多的直接控制,而無(wú)需使用匯編語(yǔ)言或硬件規(guī)格語(yǔ)言
我們來(lái)看下怎么用GLSL
同樣是一個(gè)最簡(jiǎn)單的繪制一張圖片應(yīng)用
準(zhǔn)備繪制工作
不同于GLkit 我們直接使用GLkitView 我們先自定義一個(gè)UIView的子類 作為opengl繪制的載體View
@interface GLSLDemoView : UIView
@end
重載layerClass 指定為CAEAGLLayer iOS下只有CAEAGLLayer才支持opengl的繪制
+ (Class)layerClass{
return [CAEAGLLayer class];
}
初始化opengl上下文和配置好CAEAGLLayer的相關(guān)屬性
//設(shè)置Layer
self.mEagLayer = (CAEAGLLayer *) self.layer;
self.contentScaleFactor = [UIScreen mainScreen].scale;
self.mEagLayer.opaque = YES;
self.mEagLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:NO], kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat, nil];
//設(shè)置openglES 上下文
EAGLRenderingAPI api = kEAGLRenderingAPIOpenGLES2;
EAGLContext *context = [[EAGLContext alloc]initWithAPI:api];
if (!context) {
NSLog(@"error to init openglES context");
}
if (![EAGLContext setCurrentContext:context]) {
NSLog(@"error to set current openglES context");
}
self.mContext = context;
綁定framebuffer和renderbuffer
//Render Buffer
GLuint renderBuffer;
glGenRenderbuffers(1, &renderBuffer);
self.mColorRenderBuffer = renderBuffer;
glBindRenderbuffer(GL_RENDERBUFFER, self.mColorRenderBuffer);
[self.mContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.mEagLayer];
//Frame Buffer
GLuint frameBuffer;
glGenFramebuffers(1, &frameBuffer);
self.mColorFrameBuffer = frameBuffer;
glBindFramebuffer(GL_FRAMEBUFFER, self.mColorFrameBuffer);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.mColorRenderBuffer);
常規(guī)的opengl渲染管線的準(zhǔn)備工作 接下來(lái) 我們就要開始加載我們自己的shader(著色器),
之前我們提過(guò)opengl的渲染管線中

Vetex shader 頂點(diǎn)著色器
Fragment shader 片元著色器
加載著色器
著色器自然是一種編程語(yǔ)言 我們首先要做的是編寫一個(gè)最簡(jiǎn)單的著色器 了解下著色器語(yǔ)法
GLSL的官方文檔:https://www.khronos.org/registry/OpenGL/specs/gl/GLSLangSpec.1.20.pdf
這里介紹下最簡(jiǎn)單的修飾符
const:常量值必須在聲明是初始化。它是只讀的不可修改的。
attribute:表示只讀的頂點(diǎn)數(shù)據(jù),只用在頂點(diǎn)著色器中。數(shù)據(jù)來(lái)自當(dāng)前的頂點(diǎn)狀態(tài)或者頂點(diǎn)數(shù)組。它必須是全局范圍聲明的,不能在函數(shù)內(nèi)部。一個(gè)attribute可以是浮點(diǎn)數(shù)類型的標(biāo)量,向量,或者矩陣。不可以是數(shù)組或者結(jié)構(gòu)體
uniform:一致變量。在著色器執(zhí)行期間一致變量的值是不變的。與const常量不同的是,這個(gè)值在編譯時(shí)期是未知的是由著色器外部初始化的。一致變量在頂點(diǎn)著色器和片段著色器之間是共享的。它也只能在全局范圍進(jìn)行聲明。
varying:頂點(diǎn)著色器的輸出。例如顏色或者紋理坐標(biāo),(插值后的數(shù)據(jù))作為片段著色器的只讀輸入數(shù)據(jù)。必須是全局范圍聲明的全局變量??梢允歉↑c(diǎn)數(shù)類型的標(biāo)量,向量,矩陣。不能是數(shù)組或者結(jié)構(gòu)體。
最常用的內(nèi)置變量
gl_Position:vec4類型 輸出屬性-變換后的頂點(diǎn)的位置,用于后面的固定的裁剪等操作。所有的頂點(diǎn)著色器都必須寫這個(gè)值。
gl_FragColor:vec4類型 片元著色器的輸出顏色 可用于后續(xù)使用
知道了這些最簡(jiǎn)單的修飾符和內(nèi)置變量 我們就可以編寫起自己的shader啦
首先先編寫頂點(diǎn)著色器:(一般頂點(diǎn)著色器文件以.vsh結(jié)尾)
attribute vec4 position;
attribute vec2 textCoordinate;
uniform mat4 rotateMatrix;
varying lowp vec2 varyTextCoord;
void main()
{
varyTextCoord = textCoordinate;
vec4 vPos = position;
vPos = vPos * rotateMatrix;
gl_Position = vPos;
}
rotateMatrix對(duì)應(yīng)的旋轉(zhuǎn)矩陣 可以用來(lái)向量運(yùn)算 旋轉(zhuǎn)矩陣
varing 定義了 varyTextCoord 用來(lái)傳遞紋理坐標(biāo)給片元著色器
這里的lowp表示是精度 精度要求越低 性能消耗越低
gl_Position輸出屬性-變換后的頂點(diǎn)的位置 這里做了個(gè)最簡(jiǎn)單的矩陣變換 vPos * rotateMatrix
接下來(lái)編寫片元著色器
varying lowp vec2 varyTextCoord;
uniform sampler2D colorMap;
void main()
{
gl_FragColor = texture2D(colorMap, varyTextCoord);
}
sampler2D 二維紋理的句柄也就是當(dāng)前綁定的紋理 這里我們根據(jù)頂點(diǎn)著色器傳入的紋理坐標(biāo) 僅僅做個(gè)texture2D 取色 返回紋理對(duì)應(yīng)坐標(biāo)的顏色
這樣我們就編寫好了頂點(diǎn)和片元shader 接下來(lái)我們來(lái)代碼加載shader
+ (void)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file{
NSError *error;
NSString *content = [NSString stringWithContentsOfFile:file
encoding:NSUTF8StringEncoding
error:&error];
if (error) {
NSLog(@"加載shader 本地file失敗");
return;
}
const GLchar *source = (GLchar *)[content UTF8String];
*shader = glCreateShader(type);
glShaderSource(*shader, 1, &source, NULL);
glCompileShader(*shader);
}
+ (GLuint)loadShaders:(NSString *)vshFilePath
fsh:(NSString *)fshFilePath{
GLuint vShader,fShader;
GLint program = glCreateProgram();
//編譯
[self compileShader:&vShader type:GL_VERTEX_SHADER file:vshFilePath];
[self compileShader:&fShader type:GL_FRAGMENT_SHADER file:fshFilePath];
glAttachShader(program, vShader);
glAttachShader(program, fShader);
glDeleteShader(vShader);
glDeleteShader(fShader);
return program;
}
NSString* vertFile = [[NSBundle mainBundle] pathForResource:@"shaderv" ofType:@"vsh"];
NSString* fragFile = [[NSBundle mainBundle] pathForResource:@"shaderf" ofType:@"fsh"];
self.mProgram = [GLShaderUtils loadShaders:vertFile fsh:fragFile];
編譯和綁定好了shader之后 我們開始link shader 其實(shí)這正是對(duì)應(yīng)JIT類型的語(yǔ)言的編譯 鏈接 運(yùn)行
+ (BOOL)linkProgram:(GLuint)program{
glLinkProgram(program);
GLint linkRet;
glGetProgramiv(program, GL_LINK_STATUS, &linkRet);
if (linkRet == GL_FALSE) {
GLchar messages[256];
glGetProgramInfoLog(program, sizeof(messages), 0, &messages[0]);
NSString *messageString = [NSString stringWithUTF8String:messages];
NSLog(@"error%@", messageString);
return NO;
}
else{
return YES;
}
}
如果shader寫的有誤 這里會(huì)報(bào)錯(cuò) 不過(guò)相比于xcode這些編譯器的報(bào)錯(cuò) 給到的debug信息比較弱
可以看到 加載shader是比較常規(guī)的固定代碼流程 適合封裝到工具方法中 方便復(fù)用
接下來(lái)就是創(chuàng)建VBO 也就是頂點(diǎn) 并且綁定到我們的shader上 相當(dāng)于將數(shù)據(jù)從cpu拷貝到gpu中
GLfloat attrArr[] =
{
1.0f, 1.0f, 0.0f, 1.0f, 1.0f,
-1.0f, 1.0f, 0.0f, 0.0f, 1.0f,
1.0f, -1.0f, 0.0f, 1.0f, 0.0f,
1.0f, -1.0f, 0.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f, 0.0f, 1.0f,
-1.0f, -1.0f, 0.0f, 0.0f, 0.0f,
};
GLuint attrBuffer;
glGenBuffers(1, &attrBuffer);
glBindBuffer(GL_ARRAY_BUFFER, attrBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(attrArr), attrArr, GL_DYNAMIC_DRAW);
GLuint position = glGetAttribLocation(self.mProgram, "position");
glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, NULL);
glEnableVertexAttribArray(position);
GLuint textCoor = glGetAttribLocation(self.mProgram, "textCoordinate");
glVertexAttribPointer(textCoor, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (float *)NULL + 3);
glEnableVertexAttribArray(textCoor);
這里的關(guān)鍵點(diǎn)glGetAttribLocation(self.mProgram, "position") 和 glGetAttribLocation(self.mProgram, "textCoordinate") 正是對(duì)應(yīng)的著色器代碼中的position和textCoordinate輸入變量
接下來(lái)綁定紋理
CGImageRef image = [UIImage imageNamed:imageName].CGImage;
if (!image) {
NSLog(@"load Image texture failed");
return;
}
size_t width = CGImageGetWidth(image);
size_t height = CGImageGetHeight(image);
GLubyte * imageData = (GLubyte *) calloc(width * height * 4, sizeof(GLubyte));
CGContextRef imageContext = CGBitmapContextCreate(imageData, width, height, 8, width*4,
CGImageGetColorSpace(image), kCGImageAlphaPremultipliedLast);
CGContextDrawImage(imageContext, CGRectMake(0, 0, width, height), image);
CGContextRelease(imageContext);
glBindTexture(GL_TEXTURE_2D, 0);
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
float fw = width, fh = height;
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fw, fh, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData);
glBindTexture(GL_TEXTURE_2D, 0);
free(imageData);
我們還有個(gè)旋轉(zhuǎn)矩陣變量需要設(shè)置輸入到著色器中
GLuint rotate = glGetUniformLocation(self.mProgram, "rotateMatrix");
float radians = 180 * 3.14159f / 180.0f; //旋轉(zhuǎn)180度讓圖片正向
float s = sin(radians);
float c = cos(radians);
//z軸旋轉(zhuǎn)矩陣
GLfloat zRotation[16] = {
c,-s,0,0,
s,c,0,0,
0,0,1,0,
0,0,0,1
};
//設(shè)置旋轉(zhuǎn)矩陣
glUniformMatrix4fv(rotate, 1, GL_FALSE, (GLfloat *)&zRotation[0]);
最后一步繪制
glDrawArrays(GL_TRIANGLES, 0, 6);
[self.mContext presentRenderbuffer:GL_RENDERBUFFER];
這樣我們最簡(jiǎn)單的圖片繪制就完成了

總結(jié):
著色器語(yǔ)言GLSL是一種基于C的編程語(yǔ)言 可以理解為GPU編程 方便我們操作頂點(diǎn)和片元相關(guān)的渲染管線 實(shí)現(xiàn)我們自定義的繪制效果。和普通的編程一樣。需要編寫代碼 編譯 運(yùn)行 不過(guò)shader的加載編譯運(yùn)行是在app運(yùn)行是動(dòng)態(tài)解釋執(zhí)行的。理解了著色器的運(yùn)行流程。再去編寫GLSL。會(huì)有更加深入的理解。
Demo代碼地址:LearnOpenGLESDemo
參考文章:
GLSL基礎(chǔ)語(yǔ)法介紹