案例:根據(jù)對GLSL語言的理解,自定義一個頂點(diǎn)著色器和一個片元著色器,使用著色器API完成紋理的加載。
進(jìn)階:解決紋理倒置問題。
效果如下:


準(zhǔn)備工作
- 新建iOS應(yīng)用工程,修改當(dāng)前controller的view。將原來的view繼承于UIView改成繼承于HView。
- 自定義一個HVIew類,后續(xù)繪制圖片在該類中完成。
- 新建頂點(diǎn)著色器文件和片元著色器文件。
3.1 command + N,開始新建文件。
3.2 選擇iOS->Other->Empty,新建兩個空文件,分別命名為:shaderv.vsh、shaderf.fsh
至此準(zhǔn)備工作完成,接下來就開始編碼工作。
自定義著色器
自定義著色器本質(zhì)上其實(shí)是一個字符串,但是在Xcode的編寫過程沒有任何錯誤提示,因此,在編寫過程中需要格外仔細(xì)。
- 頂點(diǎn)著色器shaderv.vsh
- 定義兩個attribute修飾符修飾的變量,分別表示頂點(diǎn)坐標(biāo)和紋理坐標(biāo)
- 定義一個varying修飾符修飾的變量,用于將紋理坐標(biāo)從頂點(diǎn)著色器傳遞給片元著色器
- main函數(shù),在該函數(shù)內(nèi)給內(nèi)建變量
gl_Position賦值。若頂點(diǎn)坐標(biāo)不需要變換,則直接將頂點(diǎn)坐標(biāo)賦值給內(nèi)建變量gl_Position。若頂點(diǎn)坐標(biāo)需要進(jìn)行變換,則將變換后的結(jié)果賦值給內(nèi)建變量gl_Position。
attribute vec4 position;
attribute vec2 textCoordinate;
varying lowp vec2 varyTextCoord;
void main(){
varyTextCoord = textCoordinate;
gl_Position = position;
}
2.片元著色器shaderf.fsh
- 指定片元著色器中float類型的精度,如果不寫,可能會報一些異常錯誤
- 定義一個與頂點(diǎn)著色器橋接的紋理坐標(biāo),寫法必須同在頂點(diǎn)著色器寫法一致,否則將無法收到從頂點(diǎn)著色器傳遞過來的數(shù)據(jù)
- 定義一個unifom修飾符修飾的變量,用于獲取紋理坐標(biāo)上每個像素點(diǎn)的紋素。
- main函數(shù),在函數(shù)內(nèi)給內(nèi)建變量
gl_FragColor賦值。通過texture2D內(nèi)建函數(shù)獲取當(dāng)前顏色值,它有兩個參數(shù):參數(shù)1:紋理圖片;參數(shù)2:紋理坐標(biāo),返回值:vec4類型的顏色值。當(dāng)顏色不需要進(jìn)行修改時,可直接將vec4類型的顏色值賦值給內(nèi)建變量gl_FragColor。當(dāng)顏色需要修改時,將最終修改的結(jié)果賦值給內(nèi)建變量gl_FragColor。
precision highp float;
varying lowp vec2 varyTextCoord;
uniform sampler2D colorMap;
void main(){
gl_FragColor = texture2D(colorMap, varyTextCoord);
}
初始化
1. 創(chuàng)建圖層
1.1 圖層主要是顯示OpenGL ES繪制內(nèi)容的載體。它的創(chuàng)建有兩種方式:
- 直接使用當(dāng)前view的layer。但是view的layer是繼承于CALayer,需要重寫類方法
layerClass,使其繼承于CAEAGLLayer。 - 直接使用[[CAEAGLLayer alloc] init]方法創(chuàng)建一個CAEAGLLayer類型的圖層,并將新創(chuàng)建的圖層添加到當(dāng)前圖層上。
self. myEagLayer = (CAEAGLLayer*)self.layer;
+ (Class)layerClass{
return [CAEAGLLayer class];
}
1.2 設(shè)置scale,這里設(shè)置當(dāng)前view的scale與屏幕的scale一樣大
[self setContentScaleFactor:[[UIScreen mainScreen] scale]];
1.3 設(shè)置描述屬性,這里設(shè)置不維持渲染內(nèi)容以及顏色格式為RGBA8
- kEAGLDrawablePropertyRetainedBacking:表示繪圖表面顯示后,是否保留其內(nèi)容,
true-保留,false-不保留。 - kEAGLDrawablePropertyColorFormat:可繪制表面的內(nèi)部顏色緩存區(qū)格式,這個key對應(yīng)的值是一個NSString指定特定顏色緩存區(qū)對象。默認(rèn)是kEAGLColorFormatRGBA8;
| 顏色緩沖區(qū)格式 | 描述 |
|---|---|
| kEAGLColorFormatRGBA8 | 32位RGBA的顏色,4*8=32位 |
| kEAGLColorFormatRGB565 | 16位RGB的顏色 |
| kEAGLColorFormatSRGBA8 | sRGB代表了標(biāo)準(zhǔn)的紅、綠、藍(lán),即CRT顯示器、LCD顯示器、投影機(jī)、打印機(jī)以及其他設(shè)備中色彩再現(xiàn)所使用的三個基本色素。sRGB的色彩空間基于獨(dú)立的色彩坐標(biāo),可以使色彩在不同的設(shè)備使用傳輸中對應(yīng)于同一個色彩坐標(biāo)體系,而不受這些設(shè)備各自具有的不同色彩坐標(biāo)的影響。 |
self.myEagLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:@false, kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatSRGBA8, kEAGLDrawablePropertyColorFormat, nil];
2. 創(chuàng)建上下文
上下文主要用來保存OpenGL ES的狀態(tài),是一個狀態(tài)機(jī),不論GLKit還是GLSL,都需要使用context。
2.1 創(chuàng)建上下文,并指定OpenGL ES渲染API的版本號
self.myContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
2.2 設(shè)置當(dāng)前上下文
[EAGLContext setCurrentContext:self.myContext];
3. 清空緩沖區(qū)
清除緩沖區(qū)的殘留數(shù)據(jù),防止其它無用數(shù)據(jù)對繪制效果造成影響
//清空渲染緩存區(qū)
glDeleteBuffers(1, &_myColorRenderBuffer);
self.myColorRenderBuffer = 0;
//清空幀緩存區(qū)
glDeleteBuffers(1, &_myColorFrameBuffer);
self.myColorFrameBuffer = 0;
4. 設(shè)置緩沖區(qū)
設(shè)置緩沖區(qū)包括設(shè)置RenderBuffer和FrameBuffer。
-
RenderBuffer:是一個通過應(yīng)用分配的2D圖像緩沖區(qū),需要附著在FrameBuffer上。
1.1 RenderBuffer有3種緩沖區(qū)- 深度緩沖區(qū)(Depth Buffer):存儲深度值等
- 紋理緩沖區(qū)(Depth Buffer):存儲紋理坐標(biāo)中對應(yīng)的紋素、顏色值等
- 模板緩沖區(qū)(Stencil Buffer):存儲模板等
1.2 設(shè)置RenderBuffer
- 定義一個緩存區(qū)ID
- 申請一個緩沖區(qū)標(biāo)志
- 將緩沖區(qū)標(biāo)識綁定到
GL_RENDERBUFFER - 綁定一個可繪制對象(layer)的存儲到一個OpenGL ES RenderBuffer對象
-(void)setupRenderBuffer{
//1.定義一個緩存區(qū)ID
GLuint buffer;
//2.申請一個緩存區(qū)標(biāo)志
glGenRenderbuffers(1, &buffer);
self.myColorRenderBuffer = buffer;
glBindRenderbuffer(GL_RENDERBUFFER, self.myColorRenderBuffer);
[self.myContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myEagLayer];
}
-
FrameBuffer:是一個收集顏色、深度、模板緩沖區(qū)的附著點(diǎn),簡稱FBO,即是一個管理者,用來管理RenderBuffer,且FrameBuffer沒有實(shí)際的存儲功能,真正實(shí)現(xiàn)存儲的是RenderBuffer。
2.1 FrameBuffer有3個附著點(diǎn)- 顏色附著點(diǎn)(Color Attachment):管理紋理、顏色緩沖區(qū)
- 深度附著點(diǎn)(depth Attachment):管理深度緩沖區(qū),會根據(jù)當(dāng)前深度緩沖中的值修改顏色緩沖中的內(nèi)容
- 模板附著點(diǎn)(Stencil Attachment):管理模板緩沖區(qū)
2.2 設(shè)置FrameBuffer
- 定義一個緩存區(qū)ID
- 申請一個緩沖區(qū)標(biāo)志
- 將緩沖區(qū)標(biāo)識綁定到GL_FRAMEBUFFER
- 通過FrameBuffer來管理RenderBuffer,將RenderBuffer附著到FrameBuffer的GL_COLOR_ATTACHMENT0附著點(diǎn)上。
-(void)setupFrameBuffer{
GLuint buffer;
glGenFramebuffers(1, &buffer);
self.myColorFrameBuffer = buffer;
glBindFramebuffer(GL_FRAMEBUFFER, self.myColorFrameBuffer);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myColorRenderBuffer);
}
注意點(diǎn):綁定renderBuffer和FrameBuffer是有順序的,先有RenderBuffer,才有FrameBuffer。
開始繪制
初始化
清除屏幕顏色,清空顏色緩沖區(qū),設(shè)置視口大小。
//設(shè)置清屏顏色
glClearColor(0.3, 0.45, 0.5, 1.0);
//清除屏幕
glClear(GL_COLOR_BUFFER_BIT);
//1.設(shè)置視口大小
CGFloat scale = [[UIScreen mainScreen] scale];
glViewport(self.frame.origin.x * scale, self.frame.origin.y * scale, self.frame.size.width * scale, self.frame.size.height * scale);
加載自定義著色器
1. 讀取并編譯頂點(diǎn)著色程序、片元著色程序
1.1 創(chuàng)建一個頂點(diǎn)/片元著色器
*shader = glCreateShader(type);
1.2 以字符串的形式將著色器源碼讀取出來,并將著色器源碼加載到著色器對象上
NSString* content = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];
const GLchar* source = (GLchar*)content.UTF8String;
glShaderSource(*shader, 1, &source, NULL);
1.3 編譯著色器,把著色器源代碼編譯成目標(biāo)代碼。此時得到一個可附著到程序的著色器對象
glCompileShader(*shader);
2. 加載著色器
2.1 創(chuàng)建program
GLint program = glCreateProgram();
2.2 將編譯好的著色器對象附著到程序中
glAttachShader(program, verShader);
glAttachShader(program, fragShader);
2.3 釋放不需要的著色器對象
glDeleteShader(verShader);
glDeleteShader(fragShader);
- 鏈接program
在鏈接之后可調(diào)用glGetProgramiv函數(shù)判斷當(dāng)前是否鏈接成功
glLinkProgram(self.myPrograme);
- 使用program
glUseProgram(self.myPrograme);
設(shè)置并處理頂點(diǎn)數(shù)據(jù)
- 設(shè)置頂點(diǎn)數(shù)據(jù)
GLfloat attrArr[] ={
0.5f, -0.5f, -1.0f, 1.0f, 0.0f,
-0.5f, 0.5f, -1.0f, 0.0f, 1.0f,
-0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
0.5f, 0.5f, -1.0f, 1.0f, 1.0f,
-0.5f, 0.5f, -1.0f, 0.0f, 1.0f,
0.5f, -0.5f, -1.0f, 1.0f, 0.0f,
};
- 申請一個頂點(diǎn)緩沖區(qū)ID,并將它綁定到GL_ARRAY_BUFFER標(biāo)識符上
GLuint attrBuffer;
glGenBuffers(1, &attrBuffer);
glBindBuffer(GL_ARRAY_BUFFER, attrBuffer);
- 把頂點(diǎn)數(shù)據(jù)從CPU拷貝到GPU
glBufferData(GL_ARRAY_BUFFER, sizeof(attrArr), attrArr, GL_DYNAMIC_DRAW);
- 打開頂點(diǎn)/片元著色器屬性通道
- 通過
glGetAttribLocation函數(shù)獲取頂點(diǎn)屬性入口,它需要兩個參數(shù),參數(shù)1:program;參數(shù)2:自定義著色器文件中變量名稱的字符串,重點(diǎn):這里的字符串必須同自定義著色器文件中變量名稱保持一致。 - 通過
glEnableVertexAttribArray函數(shù)打開著色器的屬性通道 - 通過
glVertexAttribPointer函數(shù)設(shè)置讀取方式
//設(shè)置頂點(diǎn)坐標(biāo)
GLuint position = glGetAttribLocation(self.myPrograme, "position");
glEnableVertexAttribArray(position);
glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, 5*sizeof(GLfloat), NULL);
//設(shè)置紋理坐標(biāo)
GLuint textCoor = glGetAttribLocation(self.myPrograme, "textCoordinate");
glEnableVertexAttribArray(textCoor);
glVertexAttribPointer(textCoor, 2, GL_FLOAT, GL_FALSE, 5*sizeof(GLfloat), (float *)NULL + 3);
加載紋理
加載紋理的過程是將png/jpg圖片解壓縮成位圖,并通過自定義著色器讀取每個像素點(diǎn)的紋素。
- 解壓縮png/jpg圖片,將UIImage轉(zhuǎn)換為CGImageRef。
CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
- 根據(jù)CGImageRef屬性獲取圖片的寬和高,并開辟一段空間用于存放解壓縮后的位圖信息。位圖數(shù)據(jù)的大小為寬高4。為什么是寬高4?因?yàn)閳D片共有寬高個像素點(diǎn),每個像素點(diǎn)有4個字節(jié),即RGBA,因此共有寬高*4大小的空間。
//讀取圖片的大小,寬和高
size_t width = CGImageGetWidth(spriteImage);
size_t height = CGImageGetHeight(spriteImage);
//獲取圖片字節(jié)數(shù) 寬*高*4(RGBA)
GLubyte * spriteData = (GLubyte *) calloc(width * height * 4, sizeof(GLubyte));
- 創(chuàng)建CGContextRef上下文
/*
參數(shù)1:data,指向要渲染的繪制圖像的內(nèi)存地址
參數(shù)2:width,bitmap的寬度,單位為像素
參數(shù)3:height,bitmap的高度,單位為像素
參數(shù)4:bitPerComponent,內(nèi)存中像素的每個組件的位數(shù),比如32位RGBA,就設(shè)置為8
參數(shù)5:bytesPerRow,bitmap的沒一行的內(nèi)存所占的比特數(shù)
參數(shù)6:colorSpace,bitmap上使用的顏色空間 kCGImageAlphaPremultipliedLast:RGBA
*/
CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4,CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
- 在CGContextRef上將圖片繪制出來,調(diào)用CGContextDrawImage函數(shù),使用默認(rèn)方式繪制
/*
CGContextDrawImage 使用的是Core Graphics框架,坐標(biāo)系與UIKit 不一樣。UIKit框架的原點(diǎn)在屏幕的左上角,Core Graphics框架的原點(diǎn)在屏幕的左下角。
CGContextDrawImage
參數(shù)1:繪圖上下文
參數(shù)2:rect坐標(biāo)
參數(shù)3:繪制的圖片
*/
CGContextDrawImage(spriteContext, rect, spriteImage);
- 繪制完成之后,需要將上下文釋放掉
CGContextRelease(spriteContext);
- 經(jīng)過重繪之后,就將jpg/png圖片轉(zhuǎn)換成了位圖得到了紋理數(shù)據(jù)。接下來就是載入紋理數(shù)據(jù)。
6.1 綁定紋理到默認(rèn)的紋理ID
6.2 設(shè)置紋理屬性
6.3 載入2D紋理數(shù)據(jù)
//綁定紋理到默認(rèn)的紋理ID
glBindTexture(GL_TEXTURE_2D, 0);
//設(shè)置紋理屬性
/*
參數(shù)1:紋理維度
參數(shù)2:線性過濾、為s,t坐標(biāo)設(shè)置模式
參數(shù)3:wrapMode,環(huán)繞模式
*/
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);
/*
參數(shù)1:紋理模式,GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
參數(shù)2:加載的層次,一般設(shè)置為0
參數(shù)3:紋理的顏色值GL_RGBA
參數(shù)4:寬
參數(shù)5:高
參數(shù)6:border,邊界寬度
參數(shù)7:format
參數(shù)8:type
參數(shù)9:紋理數(shù)據(jù)
*/
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
//釋放spriteData
free(spriteData);
- 設(shè)置紋理采樣器
主要是用來獲取紋理中對應(yīng)像素點(diǎn)的顏色值,即紋素。
- 通過glGetUniformLocation函數(shù)獲取片元著色器中uniform的入口。該函數(shù)需要傳入兩個參數(shù),參數(shù)1:program;參數(shù)2:在片元著色器中用uniform修飾的變量名字的字符串。
注意,該字符串必須同片元著色器中對應(yīng)的變量名保持一致。 - 使用glUniform1i函數(shù)獲取紋素,它也有兩個參數(shù),參數(shù)1:片元著色器中uniform的入口;參數(shù)2:紋理ID,默認(rèn)為0。
glUniform1i(glGetUniformLocation(self.myPrograme, "colorMap"), 0);
繪制
開始繪制,存儲到RenderBuffer,從RenderBuffer將圖片顯示到屏幕上。
- 調(diào)用glDrawArrays函數(shù),指定圖元連接方式進(jìn)行繪制
- context調(diào)用presentRenderbuffer函數(shù)將繪制好的圖片渲染到屏幕上顯示
glDrawArrays(GL_TRIANGLES, 0, 6);
[self.myContext presentRenderbuffer:GL_RENDERBUFFER];
至此,使用GLSL加載紋理已經(jīng)完成,完整代碼見Demo地址;
從效果圖上看到,圖片呈倒立顯示,這是因?yàn)?code>OpenGL要求原點(diǎn)(0,0)位于圖片的左下角,Y坐標(biāo)從下往上增加,而圖片紋理的原點(diǎn)(0,0)是位于圖片的左上角,Y坐標(biāo)從上往下增加。所以最后的照片呈上下倒置的效果。
以下是幾種解決方案:
- 方案1:將頂點(diǎn)繞Y軸進(jìn)行翻轉(zhuǎn)。這樣可以實(shí)現(xiàn)正常顯示。
問題:如何實(shí)現(xiàn)繞Y軸翻轉(zhuǎn)
解決:將頂點(diǎn)坐標(biāo)與一個旋轉(zhuǎn)矩陣相乘,得到的結(jié)果就是翻轉(zhuǎn)之后的頂點(diǎn)坐標(biāo)。
重點(diǎn):在3D課程中用的是橫向量,在OpenGL ES用的是列向量。頂點(diǎn)坐標(biāo)是一個1行4列的矩陣,因此,旋轉(zhuǎn)矩陣必須是4行4列,這樣相乘之后才能得到新的1行4列的頂點(diǎn)坐標(biāo)。另外,要實(shí)現(xiàn)翻轉(zhuǎn),只需要將該方向的坐標(biāo)數(shù)據(jù)進(jìn)行反向,如當(dāng)前需要沿X軸反向,只需要將X軸的數(shù)據(jù)全部*-1,即可將X軸的數(shù)據(jù)翻轉(zhuǎn)。
代碼詳見方案1代碼 - 方案2:可以解壓縮圖片的時候?qū)D片進(jìn)行翻轉(zhuǎn)。
解決:在context繪制的圖片,對圖片進(jìn)行翻轉(zhuǎn)。
重點(diǎn):由于翻轉(zhuǎn)之后,頂點(diǎn)數(shù)據(jù)的坐標(biāo)會發(fā)生變化,超過繪制的區(qū)域,因此在翻轉(zhuǎn)之后需要將頂點(diǎn)移至繪制區(qū)域內(nèi)。
主要使用的函數(shù)有
//先平移至合適的位置,也可以在翻轉(zhuǎn)之后再移至繪制區(qū)域內(nèi)
CGContextTranslateCTM(context, 0, height);
//將Y軸翻轉(zhuǎn)
CGContextScaleCTM(context, 1, -1);
代碼詳見方案2代碼
- 方案3:修改片元著色器紋理坐標(biāo),將片元著色器中的紋理坐標(biāo)在Y軸方向翻轉(zhuǎn)。
重點(diǎn):如何獲取紋理坐標(biāo)的Y軸方向數(shù)據(jù),通過'varyTextCoord.y'即可得到Y(jié)軸數(shù)據(jù)。將1.0-varyTextCoord.y即可實(shí)現(xiàn)翻轉(zhuǎn)。
vec2 newCoord = vec2(varyTextCoord.x, 1.0-varyTextCoord.y);
gl_FragColor = texture2D(colorMap, newCoord);
代碼詳見方案3代碼
方案4:修改頂點(diǎn)著色器紋理坐標(biāo),將頂點(diǎn)著色器的紋理坐標(biāo)在Y軸方向翻轉(zhuǎn)。
該方案原理同方案3一樣,只是在不同的著色器完成紋理坐標(biāo)的翻轉(zhuǎn)。
代碼詳見方案4代碼方案5:修改源頂點(diǎn)數(shù)據(jù)中頂點(diǎn)坐標(biāo)和紋理坐標(biāo)的映射關(guān)系。
原理同方案3、4一致,只是直接在頂點(diǎn)數(shù)組中修改源數(shù)據(jù)。
原頂點(diǎn)數(shù)據(jù)數(shù)組
GLfloat attrArr[] ={
0.5f, -0.5f, -1.0f, 1.0f, 0.0f,
-0.5f, 0.5f, -1.0f, 0.0f, 1.0f,
-0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
0.5f, 0.5f, -1.0f, 1.0f, 1.0f,
-0.5f, 0.5f, -1.0f, 0.0f, 1.0f,
0.5f, -0.5f, -1.0f, 1.0f, 0.0f,
};
修改后的頂點(diǎn)數(shù)組
GLfloat attrArr[] ={
0.5f, -0.5f, -1.0f, 1.0f, 1.0f,
-0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -1.0f, 0.0f, 1.0f,
0.5f, 0.5f, -1.0f, 1.0f, 0.0f,
-0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -1.0f, 1.0f, 1.0f,
};
代碼詳見方案5代碼