第一節中我們采樣GLKBaseEffect來繪制圖片,這次我們使用編譯鏈接自定義的著色器(shader),用簡單的GLSL語言來實現頂點、片元著色器,并對圖形進行簡單的變換。
預先說下思路,大概有以下幾步:
- 設置圖層
- 設置上下文
- 清空緩存區
- 設置RenderBuffer
- 設置FrameBuffer
- 開始繪制
好,那我們一步步按照這6步來完成最終的渲染!
前期準備
我們可以自定義一個view,在故事板中將viewController的view的類型改為此view
在自定義SFView.m中:
導入<OpenGLES/ES3/gl.h>框架
#import <OpenGLES/ES3/gl.h>
定義以下屬性:
//在iOS和tvOS上繪制OpenGL ES內容要用CAEAGLLayer圖層,它繼承于CALayer
@property(nonatomic,strong)CAEAGLLayer *myEagLayer;
@property(nonatomic,strong)EAGLContext *myContext;
@property(nonatomic,assign)GLuint myColorRenderBuffer;
@property(nonatomic,assign)GLuint myColorFrameBuffer;
@property(nonatomic,assign)GLuint myPrograme;
重寫layoutSubviews
,以上6步就在此方法中完成,當然你也可以寫到初始化方法中。
-(void)layoutSubviews
{
//1.設置圖層
[self setupLayer];
//2.設置圖形上下文
[self setupContext];
//3.清空緩存區
[self deleteRenderAndFrameBuffer];
//4.設置RenderBuffer
[self setupRenderBuffer];
//5.設置FrameBuffer
[self setupFrameBuffer];
//6.開始繪制
[self renderLayer];
}
1. 設置圖層
-(void)setupLayer
{
//將SFView的圖層從CALayer替換成CAEAGLLayer
self.myEagLayer = (CAEAGLLayer *)self.layer;
//設置放大倍數
[self setContentScaleFactor:[[UIScreen mainScreen]scale]];
//CALayer 默認是透明的,必須將它設為不透明才能將其可見。
self.myEagLayer.opaque = YES;
//設置描述屬性,這里設置不維持渲染內容以及顏色格式為RGBA8
self.myEagLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:false],kEAGLDrawablePropertyRetainedBacking,kEAGLColorFormatRGBA8,kEAGLDrawablePropertyColorFormat,nil];
}
Note:將SFView的圖層從CALayer替換成CAEAGLLayer,還需要重寫layerClass方法。
+(Class)layerClass
{
return [CAEAGLLayer class];
}
其中設置描述屬性要說明一下,CAEAGLLayer 圖層的drawableProperties
屬性要用字典設置,key為kEAGLDrawablePropertyRetainedBacking
表示繪圖表面顯示后,是否保留其內容。這個key對應的value,是一個通過NSNumber包裝的bool值。如果是false,則不保留,顯示內容后不能依賴于相同的內容,如果是ture,則保留,表示顯示后內容不變。一般只有在需要內容保存不變的情況下,才建議設置使用,但因為會導致性能降低、內存使用量增減,一般設置為flase。
key為kEAGLDrawablePropertyColorFormat
表示可繪制表面的內部顏色緩存區格式,這個key對應的value是一個NSString指定特定顏色緩存區對象,有以下幾種:
- kEAGLColorFormatRGBA8:32位RGBA的顏色,4*8=32位,默認。
- kEAGLColorFormatRGB565:16位RGB的顏色,
- kEAGLColorFormatSRGBA8:sRGB代表了標準的紅、綠、藍,即CRT顯示器、LCD顯示器、投影機、打印機以及其他設備中色彩再現所使用的三個基本色素。sRGB的色彩空間基于獨立的色彩坐標,可以使色彩在不同的設備使用傳輸中對應于同一個色彩坐標體系,而不受這些設備各自具有的不同色彩坐標的影響,不常用。
2. 設置上下文
-(void)setupContext
{
//指定API版本
EAGLRenderingAPI api = kEAGLRenderingAPIOpenGLES3;
//創建圖形上下文
EAGLContext *context = [[EAGLContext alloc]initWithAPI:api];
//判斷是否創建成功
if (!context) {
NSLog(@"上下文創建失敗");
return;
}
//設置當前圖形上下文
if (![EAGLContext setCurrentContext:context]) {
NSLog(@"設置當前圖形上下文失敗");
return;
}
self.myContext = context;
}
3. 清空緩存區
-(void)deleteRenderAndFrameBuffer
{
//刪除顏色渲染緩存區,幀緩存區
glDeleteBuffers(1, &_myColorRenderBuffer);
self.myColorRenderBuffer = 0;
//刪除幀緩存區
glDeleteBuffers(1, &_myColorFrameBuffer);
self.myColorFrameBuffer = 0;
}
你可能注意到了,上面的RenderBuffer
和FrameBuffer
是什么鬼?
buffer分為RenderBuffer
和FrameBuffer
2個大類,其中FrameBuffer
是幀緩存區,RenderBuffer
是渲染緩存區,RenderBuffer
又可分為3類,分別為colorBuffer、depthBuffer、stencilBuffer。
FrameBuffer
包含三個附著點,分別是:color Attachment(顏色附著點),depth Attachment(深度附著點),stencil Attachment(模板附著點)。
它本身不保存顏色值、深度值、 模型,而是它內部的三個附著點對應指向(類似于指針)RenderBuffer
的三個buffer:color/Texture mip(顏色紋理貼圖),depth buffer(深度緩沖區),stencil buffer(模板緩沖區),它們真正存放著顏色紋理值、深度值、 模型。所以,FrameBuffer
相當于render buffer的管理者。
4. 設置RenderBuffer
-(void)setupRenderBuffer
{
//定義一個緩存區
GLuint buffer;
//申請一個緩存區標識符
glGenRenderbuffers(1, &buffer);
self.myColorRenderBuffer = buffer;
//將標識符綁定到GL_RENDERBUFFER
glBindRenderbuffer(GL_RENDERBUFFER, self.myColorRenderBuffer);
//分配存儲空間
[self.myContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myEagLayer];
}
5. 設置FrameBuffer
-(void)setupFrameBuffer
{
//定義一個緩存區
GLuint buffer;
//申請一個緩存區標志符
glGenFramebuffers(1, &buffer);
self.myColorFrameBuffer = buffer;
//將標識符綁定到GL_FRAMEBUFFER
glBindFramebuffer(GL_FRAMEBUFFER, self.myColorFrameBuffer);
//將renderbuffer跟framebuffer進行綁定
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myColorRenderBuffer);
}
Note:frame buffer僅僅是管理者,不需要分配空間,生成空間之后,則需要將 renderbuffer跟framebuffer進行綁定,調用glFramebufferRenderbuffer函數進行綁定,后面的繪制才能起作用。
6. 開始繪制
這部分就要用GLSL編寫著色器程序了,先創建兩個empty文件,片元著色文件shaderf.fsh,頂點著色文件shaderv.vsh文件,文件后綴不重要,隨便取的。
因為程序先執行頂點著色器文件shaderv.vsh,所以我們先編寫shaderv.vsh文件。
先定義如下屬性:
attribute vec4 position;//頂點位置
attribute vec2 textCoordinate;//紋理坐標
uniform mat4 rotateMatrix;//旋轉矩陣
varying vec2 varyTextCoord;//需要傳到片元著色器的紋理坐標數據
在main函數中作如下操作:
void main()
{
//給需要傳到片元著色器的紋理數據賦值
varyTextCoord = textCoordinate;
//對頂點進行旋轉變換
vec4 vPos = position;
vPos = vPos * rotateMatrix;
//給內建變量賦值
gl_Position = vPos;
}
Note:gl_Position為內建變量,它在頂點著色器程序中必須要賦值,頂點著色器程序會計算出新的頂點,交給gl_Position。
再來編寫片元著色器文件shaderf.fsh。
先定義屬性:
varying vec2 varyTextCoord;
uniform sampler2D colorMap;//貼圖
Note:varyTextCoord是頂點著色器傳過來的,所以變量名包括修飾符都要跟頂點著色器一致。
在main函數中作如下操作:
void main()
{
gl_FragColor = texture2D(colorMap, varyTextCoord);
}
Note:gl_FragColor屬于片元著色器中的內建函數,它也必須要賦值。
再回到SFView.m中:
-(void)renderLayer
{
//設置清屏顏色
glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
//清除屏幕
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);
//2.讀取頂點著色程序、片元著色程序
NSString *vertFile = [[NSBundle mainBundle]pathForResource:@"shaderv" ofType:@"vsh"];
NSString *fragFile = [[NSBundle mainBundle]pathForResource:@"shaderf" ofType:@"fsh"];
NSLog(@"vertFile:%@",vertFile);
NSLog(@"fragFile:%@",fragFile);
//3.加載shader
self.myPrograme = [self loadShaders:vertFile Withfrag:fragFile];
//4.鏈接
glLinkProgram(self.myPrograme);
//獲取鏈接狀態
GLint linkStatus;
glGetProgramiv(self.myPrograme, GL_LINK_STATUS, &linkStatus);
if (linkStatus == GL_FALSE) {
GLchar message[512];
glGetProgramInfoLog(self.myPrograme, sizeof(message), 0, &message[0]);
NSString *messageString = [NSString stringWithUTF8String:message];
NSLog(@"Program Link Error:%@",messageString);
return;
} else {
NSLog(@"Program Link Success!");
}
//5.使用program
glUseProgram(self.myPrograme);
//設置頂點、紋理坐標,前3個是頂點坐標,后2個是紋理坐標
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,
};
/*
如果將頂點數據按如下設置,會解決渲染圖片倒置問題:
GLfloat attrArr[] =
{
0.5f, -0.5f, 0.0f, 1.0f, 1.0f, //右下
-0.5f, 0.5f, 0.0f, 0.0f, 0.0f, // 左上
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // 左下
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, // 右上
-0.5f, 0.5f, 0.0f, 0.0f, 0.0f, // 左上
0.5f, -0.5f, 0.0f, 1.0f, 1.0f, // 右下
};
*/
/頂點緩存區
GLuint attrBuffer;
//申請一個緩存區標識符
glGenBuffers(1, &attrBuffer);
//將attrBuffer綁定到GL_ARRAY_BUFFER標識符上
glBindBuffer(GL_ARRAY_BUFFER, attrBuffer);
//把頂點數據從CPU內存復制到GPU上
glBufferData(GL_ARRAY_BUFFER, sizeof(attrArr), attrArr, GL_DYNAMIC_DRAW);
//將頂點數據通過myPrograme中的傳遞到頂點著色程序的position,注意:第二參數字符串必須和shaderv.vsh中的輸入變量:position保持一致
GLuint position = glGetAttribLocation(self.myPrograme, "position");
//2.設置合適的格式從buffer里面讀取數據
glEnableVertexAttribArray(position);
//3.設置讀取方式
//參數1:index,頂點數據的索引
//參數2:size,每個頂點屬性的組件數量,1,2,3,或者4.默認初始值是4.
//參數3:type,數據中的每個組件的類型,常用的有GL_FLOAT,GL_BYTE,GL_SHORT。默認初始值為GL_FLOAT
//參數4:normalized,固定點數據值是否應該歸一化,或者直接轉換為固定值。(GL_FALSE)
//參數5:stride,連續頂點屬性之間的偏移量,默認為0;
//參數6:指定一個指針,指向數組中的第一個頂點屬性的第一個組件。默認為0
glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, NULL);
//----處理紋理數據-------
//1.glGetAttribLocation,用來獲取vertex attribute的入口的.
//注意:第二參數字符串必須和shaderv.vsh中的輸入變量:textCoordinate保持一致
GLuint textCoor = glGetAttribLocation(self.myPrograme, "textCoordinate");
//2.設置合適的格式從buffer里面讀取數據
glEnableVertexAttribArray(textCoor);
//3.設置讀取方式
//參數1:index,頂點數據的索引
//參數2:size,每個頂點屬性的組件數量,1,2,3,或者4.默認初始值是4.
//參數3:type,數據中的每個組件的類型,常用的有GL_FLOAT,GL_BYTE,GL_SHORT。默認初始值為GL_FLOAT
//參數4:normalized,固定點數據值是否應該歸一化,或者直接轉換為固定值。(GL_FALSE)
//參數5:stride,連續頂點屬性之間的偏移量,默認為0;
//參數6:指定一個指針,指向數組中的第一個頂點屬性的第一個組件。默認為0
glVertexAttribPointer(textCoor, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, (float *)NULL + 3);
//7. 加載紋理
[self setupTexture:@"SF-1"];
//rotate取的是shaderv.vsh中的uniform屬性,rotateMatrix,注意,想要獲取shader里面的變量,這里記得要。在glLinkProgram后面
GLuint rotate = glGetUniformLocation(self.myPrograme, "rotateMatrix");
//獲取弧度
float radians = 10 * 3.14159f / 180.0f;
//弧度對于的sin\cos值
float s = sin(radians);
float c = cos(radians);
//z軸旋轉矩陣
GLfloat zRotation[16] = {
c, -s, 0, 0,
s, c, 0, 0,
0, 0, 1.0, 0,
0.0, 0, 0, 1.0
};
//設置旋轉矩陣
glUniformMatrix4fv(rotate, 1, GL_FALSE, (GLfloat *)&zRotation[0]);
glDrawArrays(GL_TRIANGLES, 0, 6);
//將渲染緩存區中的數據渲染到上下文
[self.myContext presentRenderbuffer:GL_RENDERBUFFER];
}
其中,第3步加載shader方法如下:
-(GLuint)loadShaders:(NSString *)vert Withfrag:(NSString *)frag
{
//定義2個零時著色器對象
GLuint verShader, fragShader;
//創建program
GLint program = glCreateProgram();
//編譯頂點著色程序、片元著色器程序
//參數1:編譯完存儲的底層地址
//參數2:編譯的類型,GL_VERTEX_SHADER(頂點)、GL_FRAGMENT_SHADER(片元)
//參數3:文件路徑
[self compileShader:&verShader type:GL_VERTEX_SHADER file:vert];
[self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:frag];
//創建最終的程序
glAttachShader(program, verShader);
glAttachShader(program, fragShader);
//釋放不需要的shader
glDeleteShader(verShader);
glDeleteShader(fragShader);
return program;
}
//鏈接shader
- (void)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file{
//讀取文件路徑字符串
NSString* content = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];
const GLchar* source = (GLchar *)[content UTF8String];
//創建一個shader(根據type類型)
*shader = glCreateShader(type);
//將頂點著色器源碼附加到著色器對象上。
//參數1:shader,要編譯的著色器對象 *shader
//參數2:numOfStrings,傳遞的源碼字符串數量 1個
//參數3:strings,著色器程序的源碼(真正的著色器程序源碼)
//參數4:lenOfStrings,長度,字符串數組的長度
glShaderSource(*shader, 1, &source,NULL);
//把著色器源代碼編譯成目標代碼
glCompileShader(*shader);
}
其中,第7步加載紋理方法如下:
- (GLuint)setupTexture:(NSString *)fileName {
//1、獲取圖片的CGImageRef
CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
//判斷圖片是否獲取成功
if (!spriteImage) {
NSLog(@"Failed to load image %@", fileName);
exit(1);
}
//2、讀取圖片的大小,寬和高
size_t width = CGImageGetWidth(spriteImage);
size_t height = CGImageGetHeight(spriteImage);
//3.獲取圖片字節數 寬*高*4(RGBA)
GLubyte * spriteData = (GLubyte *) calloc(width * height * 4, sizeof(GLubyte));
//4.創建上下文
/*
參數1:data,指向要渲染的繪制圖像的內存地址
參數2:width,bitmap的寬度,單位為像素
參數3:height,bitmap的高度,單位為像素
參數4:bitPerComponent,內存中像素的每個組件的位數,比如32位RGBA,就設置為8
參數5:bytesPerRow,bitmap的每一行的內存所占的比特數
參數6:colorSpace,bitmap上使用的顏色空間 kCGImageAlphaPremultipliedLast:RGBA
*/
CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4,CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
//5、在CGContextRef上繪圖
/*
CGContextDrawImage 使用的是Core Graphics框架,坐標系與UIKit 不一樣。UIKit框架的原點在屏幕的左上角,Core Graphics框架的原點在屏幕的左下角。
CGContextDrawImage
參數1:繪圖上下文
參數2:rect坐標
參數3:繪制的圖片
*/
CGRect rect = CGRectMake(0, 0, width, height);
//繪制,發現圖片是倒的。
CGContextDrawImage(spriteContext, CGRectMake(0, 0, width, height), spriteImage);
/*
解決圖片倒置的方法:
CGContextTranslateCTM(spriteContext, rect.origin.x, rect.origin.y);
CGContextTranslateCTM(spriteContext, 0, rect.size.height);
CGContextScaleCTM(spriteContext, 1.0, -1.0);
CGContextTranslateCTM(spriteContext, -rect.origin.x, -rect.origin.y);
CGContextDrawImage(spriteContext, rect, spriteImage);
*/
//6、畫圖完畢就釋放上下文
CGContextRelease(spriteContext);
//5、綁定紋理到默認的紋理ID(這里只有一張圖片,故而相當于默認于片元著色器里面的colorMap,如果有多張圖不可以這么做)
glBindTexture(GL_TEXTURE_2D, 0);
//設置紋理屬性
/*
參數1:紋理維度
參數2:線性過濾、為s,t坐標設置模式
參數3:wrapMode,環繞模式
*/
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;
//載入紋理2D數據
/*
參數1:紋理模式,GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
參數2:加載的層次,一般設置為0
參數3:紋理的顏色值GL_RGBA
參數4:寬
參數5:高
參數6:border,邊界寬度
參數7:format
參數8:type
參數9:紋理數據
*/
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fw, fh, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
//綁定紋理
/*
參數1:紋理維度
參數2:紋理ID,因為只有一個紋理,給0就可以了。
*/
glBindTexture(GL_TEXTURE_2D, 0);
//釋放spriteData
free(spriteData);
return 0;
}
至此,自定義的view算是完成了,最后在viewController中調用一下:
- (void)viewDidLoad {
[super viewDidLoad];
self.myView = (SFView *)self.view;
}
最終效果
可以發現,圖片是倒置的,解決這個問題的方案也在代碼中有所提及,大家可以嘗試一下。