案例:根據(jù)對GLSL語言的理解蒂誉,自定義一個頂點著色器和一個片元著色器怖侦,使用著色器API完成紋理的加載篡悟。
進階:解決紋理倒置問題。
效果如下:
準備工作
- 新建iOS應用工程匾寝,修改當前controller的view搬葬。將原來的view繼承于UIView改成繼承于HView。
- 自定義一個HVIew類艳悔,后續(xù)繪制圖片在該類中完成急凰。
- 新建頂點著色器文件和片元著色器文件。
3.1 command + N猜年,開始新建文件抡锈。
3.2 選擇iOS->Other->Empty,新建兩個空文件乔外,分別命名為:shaderv.vsh床三、shaderf.fsh
至此準備工作完成,接下來就開始編碼工作杨幼。
自定義著色器
自定義著色器本質上其實是一個字符串撇簿,但是在Xcode的編寫過程沒有任何錯誤提示,因此差购,在編寫過程中需要格外仔細补疑。
- 頂點著色器shaderv.vsh
- 定義兩個attribute修飾符修飾的變量,分別表示頂點坐標和紋理坐標
- 定義一個varying修飾符修飾的變量歹撒,用于將紋理坐標從頂點著色器傳遞給片元著色器
- main函數(shù)莲组,在該函數(shù)內給內建變量
gl_Position
賦值。若頂點坐標不需要變換暖夭,則直接將頂點坐標賦值給內建變量gl_Position锹杈。若頂點坐標需要進行變換,則將變換后的結果賦值給內建變量gl_Position迈着。
attribute vec4 position;
attribute vec2 textCoordinate;
varying lowp vec2 varyTextCoord;
void main(){
varyTextCoord = textCoordinate;
gl_Position = position;
}
2.片元著色器shaderf.fsh
- 指定片元著色器中float類型的精度竭望,如果不寫,可能會報一些異常錯誤
- 定義一個與頂點著色器橋接的紋理坐標裕菠,寫法必須同在頂點著色器寫法一致咬清,否則將無法收到從頂點著色器傳遞過來的數(shù)據(jù)
- 定義一個unifom修飾符修飾的變量,用于獲取紋理坐標上每個像素點的紋素。
- main函數(shù)旧烧,在函數(shù)內給內建變量
gl_FragColor
賦值影钉。通過texture2D內建函數(shù)獲取當前顏色值,它有兩個參數(shù):參數(shù)1:紋理圖片掘剪;參數(shù)2:紋理坐標平委,返回值:vec4類型的顏色值。當顏色不需要進行修改時夺谁,可直接將vec4類型的顏色值賦值給內建變量gl_FragColor廉赔。當顏色需要修改時,將最終修改的結果賦值給內建變量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繪制內容的載體蜡塌。它的創(chuàng)建有兩種方式:
- 直接使用當前view的layer。但是view的layer是繼承于CALayer勿负,需要重寫類方法
layerClass
岗照,使其繼承于CAEAGLLayer
。 - 直接使用[[CAEAGLLayer alloc] init]方法創(chuàng)建一個CAEAGLLayer類型的圖層笆环,并將新創(chuàng)建的圖層添加到當前圖層上攒至。
self. myEagLayer = (CAEAGLLayer*)self.layer;
+ (Class)layerClass{
return [CAEAGLLayer class];
}
1.2 設置scale,這里設置當前view的scale與屏幕的scale一樣大
[self setContentScaleFactor:[[UIScreen mainScreen] scale]];
1.3 設置描述屬性躁劣,這里設置不維持渲染內容以及顏色格式為RGBA8
- kEAGLDrawablePropertyRetainedBacking:表示繪圖表面顯示后迫吐,是否保留其內容,
true-保留账忘,false-不保留
志膀。 - kEAGLDrawablePropertyColorFormat:可繪制表面的內部顏色緩存區(qū)格式,這個key對應的值是一個NSString指定特定顏色緩存區(qū)對象鳖擒。默認是kEAGLColorFormatRGBA8溉浙;
顏色緩沖區(qū)格式 | 描述 |
---|---|
kEAGLColorFormatRGBA8 | 32位RGBA的顏色,4*8=32位 |
kEAGLColorFormatRGB565 | 16位RGB的顏色 |
kEAGLColorFormatSRGBA8 | sRGB代表了標準的紅蒋荚、綠戳稽、藍,即CRT顯示器期升、LCD顯示器惊奇、投影機、打印機以及其他設備中色彩再現(xiàn)所使用的三個基本色素播赁。sRGB的色彩空間基于獨立的色彩坐標颂郎,可以使色彩在不同的設備使用傳輸中對應于同一個色彩坐標體系,而不受這些設備各自具有的不同色彩坐標的影響容为。 |
self.myEagLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:@false, kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatSRGBA8, kEAGLDrawablePropertyColorFormat, nil];
2. 創(chuàng)建上下文
上下文主要用來保存OpenGL ES的狀態(tài)乓序,是一個狀態(tài)機寺酪,不論GLKit還是GLSL,都需要使用context替劈。
2.1 創(chuàng)建上下文寄雀,并指定OpenGL ES渲染API的版本號
self.myContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
2.2 設置當前上下文
[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. 設置緩沖區(qū)
設置緩沖區(qū)包括設置RenderBuffer和FrameBuffer抬纸。
-
RenderBuffer:是一個通過應用分配的2D圖像緩沖區(qū),需要附著在FrameBuffer上耿戚。
1.1 RenderBuffer有3種緩沖區(qū)- 深度緩沖區(qū)(Depth Buffer):存儲深度值等
- 紋理緩沖區(qū)(Depth Buffer):存儲紋理坐標中對應的紋素湿故、顏色值等
- 模板緩沖區(qū)(Stencil Buffer):存儲模板等
1.2 設置RenderBuffer
- 定義一個緩存區(qū)ID
- 申請一個緩沖區(qū)標志
- 將緩沖區(qū)標識綁定到
GL_RENDERBUFFER
- 綁定一個可繪制對象(layer)的存儲到一個OpenGL ES RenderBuffer對象
-(void)setupRenderBuffer{
//1.定義一個緩存區(qū)ID
GLuint buffer;
//2.申請一個緩存區(qū)標志
glGenRenderbuffers(1, &buffer);
self.myColorRenderBuffer = buffer;
glBindRenderbuffer(GL_RENDERBUFFER, self.myColorRenderBuffer);
[self.myContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myEagLayer];
}
-
FrameBuffer:是一個收集顏色、深度膜蛔、模板緩沖區(qū)的附著點坛猪,簡稱FBO,即是一個管理者皂股,用來管理RenderBuffer墅茉,且FrameBuffer沒有實際的存儲功能,真正實現(xiàn)存儲的是RenderBuffer呜呐。
2.1 FrameBuffer有3個附著點- 顏色附著點(Color Attachment):管理紋理就斤、顏色緩沖區(qū)
- 深度附著點(depth Attachment):管理深度緩沖區(qū),會根據(jù)當前深度緩沖中的值修改顏色緩沖中的內容
- 模板附著點(Stencil Attachment):管理模板緩沖區(qū)
2.2 設置FrameBuffer
- 定義一個緩存區(qū)ID
- 申請一個緩沖區(qū)標志
- 將緩沖區(qū)標識綁定到GL_FRAMEBUFFER
- 通過FrameBuffer來管理RenderBuffer蘑辑,將RenderBuffer附著到FrameBuffer的GL_COLOR_ATTACHMENT0附著點上洋机。
-(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);
}
注意點:綁定renderBuffer和FrameBuffer是有順序的,先有RenderBuffer洋魂,才有FrameBuffer绷旗。
開始繪制
初始化
清除屏幕顏色,清空顏色緩沖區(qū)副砍,設置視口大小衔肢。
//設置清屏顏色
glClearColor(0.3, 0.45, 0.5, 1.0);
//清除屏幕
glClear(GL_COLOR_BUFFER_BIT);
//1.設置視口大小
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. 讀取并編譯頂點著色程序、片元著色程序
1.1 創(chuàng)建一個頂點/片元著色器
*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 編譯著色器角骤,把著色器源代碼編譯成目標代碼。此時得到一個可附著到程序的著色器對象
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
在鏈接之后可調用glGetProgramiv
函數(shù)判斷當前是否鏈接成功
glLinkProgram(self.myPrograme);
- 使用program
glUseProgram(self.myPrograme);
設置并處理頂點數(shù)據(jù)
- 設置頂點數(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,
};
- 申請一個頂點緩沖區(qū)ID心剥,并將它綁定到GL_ARRAY_BUFFER標識符上
GLuint attrBuffer;
glGenBuffers(1, &attrBuffer);
glBindBuffer(GL_ARRAY_BUFFER, attrBuffer);
- 把頂點數(shù)據(jù)從CPU拷貝到GPU
glBufferData(GL_ARRAY_BUFFER, sizeof(attrArr), attrArr, GL_DYNAMIC_DRAW);
- 打開頂點/片元著色器屬性通道
- 通過
glGetAttribLocation
函數(shù)獲取頂點屬性入口启搂,它需要兩個參數(shù),參數(shù)1:program刘陶;參數(shù)2:自定義著色器文件中變量名稱的字符串胳赌,重點:這里的字符串必須同自定義著色器文件中變量名稱保持一致
。 - 通過
glEnableVertexAttribArray
函數(shù)打開著色器的屬性通道 - 通過
glVertexAttribPointer
函數(shù)設置讀取方式
//設置頂點坐標
GLuint position = glGetAttribLocation(self.myPrograme, "position");
glEnableVertexAttribArray(position);
glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, 5*sizeof(GLfloat), NULL);
//設置紋理坐標
GLuint textCoor = glGetAttribLocation(self.myPrograme, "textCoordinate");
glEnableVertexAttribArray(textCoor);
glVertexAttribPointer(textCoor, 2, GL_FLOAT, GL_FALSE, 5*sizeof(GLfloat), (float *)NULL + 3);
加載紋理
加載紋理的過程是將png/jpg圖片解壓縮成位圖匙隔,并通過自定義著色器讀取每個像素點的紋素疑苫。
- 解壓縮png/jpg圖片,將UIImage轉換為CGImageRef。
CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
- 根據(jù)CGImageRef屬性獲取圖片的寬和高捍掺,并開辟一段空間用于存放解壓縮后的位圖信息撼短。位圖數(shù)據(jù)的大小為寬高4。為什么是寬高4挺勿?因為圖片共有寬高個像素點曲横,每個像素點有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,指向要渲染的繪制圖像的內存地址
參數(shù)2:width,bitmap的寬度蚊丐,單位為像素
參數(shù)3:height,bitmap的高度熙参,單位為像素
參數(shù)4:bitPerComponent,內存中像素的每個組件的位數(shù),比如32位RGBA麦备,就設置為8
參數(shù)5:bytesPerRow,bitmap的沒一行的內存所占的比特數(shù)
參數(shù)6:colorSpace,bitmap上使用的顏色空間 kCGImageAlphaPremultipliedLast:RGBA
*/
CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4,CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
- 在CGContextRef上將圖片繪制出來孽椰,調用CGContextDrawImage函數(shù),使用默認方式繪制
/*
CGContextDrawImage 使用的是Core Graphics框架凛篙,坐標系與UIKit 不一樣黍匾。UIKit框架的原點在屏幕的左上角,Core Graphics框架的原點在屏幕的左下角呛梆。
CGContextDrawImage
參數(shù)1:繪圖上下文
參數(shù)2:rect坐標
參數(shù)3:繪制的圖片
*/
CGContextDrawImage(spriteContext, rect, spriteImage);
- 繪制完成之后膀捷,需要將上下文釋放掉
CGContextRelease(spriteContext);
- 經過重繪之后,就將jpg/png圖片轉換成了位圖得到了紋理數(shù)據(jù)削彬。接下來就是載入紋理數(shù)據(jù)全庸。
6.1 綁定紋理到默認的紋理ID
6.2 設置紋理屬性
6.3 載入2D紋理數(shù)據(jù)
//綁定紋理到默認的紋理ID
glBindTexture(GL_TEXTURE_2D, 0);
//設置紋理屬性
/*
參數(shù)1:紋理維度
參數(shù)2:線性過濾、為s,t坐標設置模式
參數(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:加載的層次雁刷,一般設置為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);
- 設置紋理采樣器
主要是用來獲取紋理中對應像素點的顏色值,即紋素沛励。
- 通過glGetUniformLocation函數(shù)獲取片元著色器中uniform的入口责语。該函數(shù)需要傳入兩個參數(shù),參數(shù)1:program目派;參數(shù)2:在片元著色器中用uniform修飾的變量名字的字符串坤候。
注意,該字符串必須同片元著色器中對應的變量名保持一致
企蹭。 - 使用glUniform1i函數(shù)獲取紋素白筹,它也有兩個參數(shù)智末,參數(shù)1:片元著色器中uniform的入口;參數(shù)2:紋理ID徒河,默認為0系馆。
glUniform1i(glGetUniformLocation(self.myPrograme, "colorMap"), 0);
繪制
開始繪制,存儲到RenderBuffer顽照,從RenderBuffer將圖片顯示到屏幕上由蘑。
- 調用glDrawArrays函數(shù),指定圖元連接方式進行繪制
- context調用presentRenderbuffer函數(shù)將繪制好的圖片渲染到屏幕上顯示
glDrawArrays(GL_TRIANGLES, 0, 6);
[self.myContext presentRenderbuffer:GL_RENDERBUFFER];
至此代兵,使用GLSL加載紋理已經完成尼酿,完整代碼見Demo地址;
從效果圖上看到奢人,圖片呈倒立顯示谓媒,這是因為OpenGL要求原點(0,0)位于圖片的左下角淆院,Y坐標從下往上增加何乎,而圖片紋理的原點(0,0)是位于圖片的左上角,Y坐標從上往下增加
土辩。所以最后的照片呈上下倒置的效果支救。
以下是幾種解決方案:
- 方案1:將頂點繞Y軸進行翻轉。這樣可以實現(xiàn)正常顯示拷淘。
問題:如何實現(xiàn)繞Y軸翻轉
解決:將頂點坐標與一個旋轉矩陣相乘各墨,得到的結果就是翻轉之后的頂點坐標。
重點:在3D課程中用的是橫向量启涯,在OpenGL ES用的是列向量贬堵。頂點坐標是一個1行4列的矩陣,因此结洼,旋轉矩陣必須是4行4列黎做,這樣相乘之后才能得到新的1行4列的頂點坐標。另外松忍,要實現(xiàn)翻轉蒸殿,只需要將該方向的坐標數(shù)據(jù)進行反向,如當前需要沿X軸反向鸣峭,只需要將X軸的數(shù)據(jù)全部*-1宏所,即可將X軸的數(shù)據(jù)翻轉。
代碼詳見方案1代碼 - 方案2:可以解壓縮圖片的時候對圖片進行翻轉摊溶。
解決:在context繪制的圖片爬骤,對圖片進行翻轉。
重點:由于翻轉之后莫换,頂點數(shù)據(jù)的坐標會發(fā)生變化盖腕,超過繪制的區(qū)域赫冬,因此在翻轉之后需要將頂點移至繪制區(qū)域內。
主要使用的函數(shù)有
//先平移至合適的位置溃列,也可以在翻轉之后再移至繪制區(qū)域內
CGContextTranslateCTM(context, 0, height);
//將Y軸翻轉
CGContextScaleCTM(context, 1, -1);
代碼詳見方案2代碼
- 方案3:修改片元著色器紋理坐標劲厌,將片元著色器中的紋理坐標在Y軸方向翻轉。
重點:如何獲取紋理坐標的Y軸方向數(shù)據(jù)听隐,通過'varyTextCoord.y'即可得到Y軸數(shù)據(jù)补鼻。將1.0-varyTextCoord.y即可實現(xiàn)翻轉。
vec2 newCoord = vec2(varyTextCoord.x, 1.0-varyTextCoord.y);
gl_FragColor = texture2D(colorMap, newCoord);
代碼詳見方案3代碼
方案4:修改頂點著色器紋理坐標雅任,將頂點著色器的紋理坐標在Y軸方向翻轉风范。
該方案原理同方案3一樣,只是在不同的著色器完成紋理坐標的翻轉沪么。
代碼詳見方案4代碼方案5:修改源頂點數(shù)據(jù)中頂點坐標和紋理坐標的映射關系硼婿。
原理同方案3、4一致禽车,只是直接在頂點數(shù)組中修改源數(shù)據(jù)寇漫。
原頂點數(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,
};
修改后的頂點數(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代碼