前面的博客沒有過多的涉及代碼,這篇博客就聊聊渲染紋理,和OpenGL ES2.0 API的使用 和 步驟。編程
什麼是紋理? 紋理能夠是一張圖片也能夠是顏色,就好像咱們穿着的那一層衣服,他就是紋理;你在赤裸的身體上用一種或多種顏料畫滿了畫,這層畫也是紋理,他就是最外層的包裝。swift
什麼是紋理渲染?,就是在將那層紋理轉換成famebuffer內帶顏色的像素點,顯示在屏幕上,錯落不一樣的顏色像素點,構成了咱們看到的畫面。因此說,紋理就是以不一樣顏色的形式呈如今屏幕上。api
紋理圖片
相機:哈蘇500cm
膠片:Kodak Ektar100
環境
操做系統:macOS 10.15.5
IDE:Xcode 11.5
運行設備:Simulator iPhone SE 2nd
執行結果 數組
接下來按照這個流程來進行代碼編寫,從 開始設置圖層CAEAGLLayer->完成FrameBuffer 看成是渲染的準備工做,最後一步渲染會繼續拆分緩存
#import <OpenGLES/ES2/gl.h>
markdown
@interface DrawView()
/// 在iOS和tvOS上繪製OpenGL ES內容的圖層,CAEAGLLayer繼承於CALayer
@property(nonatomic,strong)CAEAGLLayer *myEagLayer;
/// 建立的上下文
@property(nonatomic,strong)EAGLContext *myContext;
/// 渲染緩存區 (實際上是該緩存區的ID,經過ID能夠訪問該緩存區,類比指針)
@property(nonatomic,assign)GLuint myColorRenderBuffer;
/// 幀緩存區 (同上)
@property(nonatomic,assign)GLuint myColorFrameBuffer;
/// 自定義的可編程管線,編譯並附着了頂點着色器和片元着色器,能夠將CPU內的頂點等其餘數據傳入自定義着色器
@property(nonatomic,assign)GLuint myPrograme;
@end
複製代碼
重寫layerClass 將self.layer從CALayer變成->CAEAGLLayer框架
// 代碼
+ (Class)layerClass
{
return [CAEAGLLayer class];
}
複製代碼
開始設置圖層ide
// 代碼
- (void)setupLayer
{
//1.建立特殊圖層
self.myEagLayer = (CAEAGLLayer *)self.layer;
//2.設置scale
[self setContentScaleFactor:[[UIScreen mainScreen]scale]];
//3.設置描述屬性,這裏設置不維持渲染內容以及顏色格式爲RGBA8
NSDictionary *drawableProperties = @{
kEAGLDrawablePropertyRetainedBacking: @false,
kEAGLDrawablePropertyColorFormat : kEAGLColorFormatRGBA8
};
self.myEagLayer.drawableProperties = drawableProperties;
}
複製代碼
CAEAGLLayer是Apple在QuartzCore裏爲咱們提供的繪製圖層,咱們繪製的紋理等能夠繪製在這個圖層,他繼承於CALayer,而且繼承了協議EAGLDrawable 函數
drawableProperties中kEAGLDrawablePropertyRetainedBacking表示繪圖表面顯示後,是否保留其內容
drawableProperties中的kEAGLDrawablePropertyColorFormat表示繪製表面的內部顏色緩存區格式 下面這是Apple給出的定義ui
/************************************************************************/
/* Values for kEAGLDrawablePropertyColorFormat key */
/************************************************************************/
EAGL_EXTERN NSString * const kEAGLColorFormatRGBA8; // 32位RGBA的顏色,4*8=32位
EAGL_EXTERN NSString * const kEAGLColorFormatRGB565;// 16位RGB的顏色
EAGL_EXTERN NSString * const kEAGLColorFormatSRGBA8 // sRGB表明了標準的紅、綠、藍,即CRT顯示器、LCD顯示器、投影機、打印機以及其餘設備中色彩再現所使用的三個基本色素。sRGB的色彩空間基於獨立的色彩座標,可使色彩在不一樣的設備使用傳輸中對應於同一個色彩座標體系,而不受這些設備各自具備的不一樣色彩座標的影響。NS_AVAILABLE_IOS(7_0);
複製代碼
-(void)setupContext
{
//1.指定OpenGL ES 渲染API版本,咱們使用2.0
EAGLRenderingAPI api = kEAGLRenderingAPIOpenGLES2;
//2.建立圖形上下文
EAGLContext *context = [[EAGLContext alloc]initWithAPI:api];
//3.判斷是否建立成功
if (!context) {
NSLog(@"Create context failed!");
return;
}
//4.設置圖形上下文
if (![EAGLContext setCurrentContext:context]) {
NSLog(@"setCurrentContext failed!");
return;
}
//5.將局部context,變成全局的
self.myContext = context;
}
複製代碼
-(void)deleteRenderAndFrameBuffer
{
/*
buffer分爲frame buffer 和 render buffer2個大類。
其中frame buffer 至關於render buffer的管理者。
frame buffer object即稱FBO。
render buffer則又可分爲3類。colorBuffer、depthBuffer、stencilBuffer。
*/
glDeleteBuffers(1, &_myColorRenderBuffer);
self.myColorRenderBuffer = 0;
glDeleteBuffers(1, &_myColorFrameBuffer);
self.myColorFrameBuffer = 0;
}
複製代碼
在這裏,出現了兩個緩存區:framebuffer 、 renderbuffer
framebuffer在離屏渲染的時候曾提到過,這是一個最終繪製完成的緩存區,當屏幕刷新控制器刷新(60Hz)時,從framebuffer交給屏幕顯示。 一個framebuffer object 對象又被稱爲FBO,下面均使用FBO
renderbuffer是什麼呢?下圖是蘋果官方提供的framebuffer和renderbuffer的關係圖。renderbuffer有三種:colorbuffer(顏色)、depthbuffer(深度)、texture(紋理)。這三種分別用來存放color、depth、texture。FBO則提供了三個附着點 GL_COLOR_ATTACHMENT0 、 GL_DEPTH_ATTACHMENT 、GL_STENCIL_ATTACHMENT 分別用來掛載(綁定) colorbuffer object 、depthbuffer object 、 texture。
圖中只畫出了GL_COLOR_ATTACHMENT0和GL_DEPTH_ATTACHMENT
- (void)setupRenderBuffer
{
//1.定義一個緩存區ID
GLuint buffer;
//2.申請一個緩存區標誌
glGenRenderbuffers(1, &buffer);
//3.
self.myColorRenderBuffer = buffer;
//4.將標識符綁定到GL_RENDERBUFFER
glBindRenderbuffer(GL_RENDERBUFFER, self.myColorRenderBuffer);
//5.將可繪製對象drawable object's CAEAGLLayer的存儲綁定到OpenGL ES renderBuffer對象
[self.myContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myEagLayer];
}
複製代碼
蘋果官方文檔也講的很清楚:申請、綁定、 存儲到self.myEagLayer
- (void)setupFrameBuffer
{
//1.定義一個緩存區ID
GLuint buffer;
//2.申請一個緩存區標誌
glGenBuffers(1, &buffer);
//3.
self.myColorFrameBuffer = buffer;
//4. 將申請的緩存區標誌和緩存區綁定,ID即該緩存區,經過使用ID就是在使用緩存區
glBindFramebuffer(GL_FRAMEBUFFER, self.myColorFrameBuffer);
/*生成幀緩存區以後,則須要將renderbuffer跟framebuffer進行綁定,
調用glFramebufferRenderbuffer函數進行綁定到對應的附着點上,後面的繪製才能起做用
*/
//5.將渲染緩存區myColorRenderBuffer 經過glFramebufferRenderbuffer函數綁定到 GL_COLOR_ATTACHMENT0上。
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myColorRenderBuffer);
}
複製代碼
步驟和前面的renderbuffer差很少
這一步算是比較核心,須要準備繪製圖形的編譯着色器->使用program->座標——頂點數據->圖片解碼->加載紋理->提交framebuffer顯示。
這一步的方法,咱們命名爲- (void)renderDraw {}
, 下面的代碼塊和用到完整方法均是在此方法內執行或調用
- (void)renderDraw
{
// 常規基礎操做:清除背景色、設置視口
// 讀取頂點着色器、片元着色器
// 加載編譯後的頂點、片元着色器
// 連接、使用附着了兩大着色器的program
// 設置頂點座標和紋理座標
// 處理頂點:申請頂點緩存區、
// 加載紋理:(圖片解碼)
// 繪製、提交顯示
}
複製代碼
// renderDraw的代碼
//設置清屏顏色
glClearColor(0.3f, 0.45f, 0.5f, 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);
複製代碼
咱們熟悉的美顏相機的各類濾鏡,其實就是用GLSL語言自定義頂點或者片元着色器,在GPUImage的代碼裏,能看到不一樣濾鏡的着色器代碼。可是怎樣用GLSL去寫一個自定義濾鏡,後面開專題案例,也能夠參考GPUImage,它裏面有很是豐富的GLSL濾鏡代碼。
// renderDraw的代碼
// 讀取自定義的兩個着色器文件
NSString *vertFile = [[NSBundle mainBundle]pathForResource:@"shaderv" ofType:@"vsh"];
NSString *fragFile = [[NSBundle mainBundle]pathForResource:@"shaderf" ofType:@"fsh"];
複製代碼
這裏須要說明一下,由於Xcode不支持編譯GLSL,因此頂點着色器和片元着色器就是兩段字符串文本,我只是將他們單獨寫在兩個文件裏,再讀取。事實上這兩段文本也能夠以其餘的形態存在,在GPUImage中,這兩個着色器就是以C字符串的形式同時定義在.m文件裏,只要拿到的是字符串,能知足OpenGL指定的方法去編譯便可
上面也講了,Xcode不支持編譯着色器,咱們須要OpenGL本身去編譯,而後附着到program上,點到爲止,剩下的事情,由program繼續完成。因此這一步,咱們的任務是:將着色器字符串編譯->附着到program。
咱們先封裝一個着色器編譯方法compileShader:type:file:
// 着色器編譯
//shader:編譯完存儲的底層地址
//type:編譯的類型,GL_VERTEX_SHADER(頂點)、GL_FRAGMENT_SHADER(片元)
//file:文件路徑
- (void)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file
{
//1.讀取文件路徑字符串
NSString* content = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];
const GLchar* source = (GLchar *)[content UTF8String];
//2.建立一個shader(根據type類型)
*shader = glCreateShader(type);
//3.將着色器源碼附加到着色器對象上。
//參數1:shader,要編譯的着色器對象 *shader
//參數2:numOfStrings,傳遞的源碼字符串數量 1個
//參數3:strings,着色器程序的源碼(真正的着色器程序源碼)
//參數4:lenOfStrings,長度,具備每一個字符串長度的數組,或NULL,這意味着字符串是NULL終止的
glShaderSource(*shader, 1, &source,NULL);
//4.把着色器源代碼編譯成目標代碼
glCompileShader(*shader);
}
複製代碼
這段代碼就幹了三件事:
回到renderDraw方法,這是咱們整個渲染模塊的主函數。
// renderDraw的代碼
// 一、定義兩個着色器
GLuint verShader, fragShader;
// 二、編譯頂點和片元着色器
[self compileShader:&verShader type:GL_VERTEX_SHADER file:vert];
[self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:frag];
// 三、建立program
self.myPrograme = glCreateProgram();
// 四、將編譯的着色器附着到program
glAttachShader(program, verShader);
glAttachShader(program, fragShader);
// 5.釋放不須要的shader
glDeleteShader(verShader);
glDeleteShader(fragShader);
複製代碼
此刻咱們就已經拿到了附着了編譯好着色器的program——self.myPrograme。 在拿到編譯好的shader後,這段代碼其實就幹了兩件事,目的是獲取program:
上面已經提到Program,Program幹什麼,打個比方:她就像是擁有了兩項已經練成(編譯附着)的絕世武功(着色器),而咱們又不會,那咱們只有經過告訴她作什麼,間接地使用了這兩項絕世武功(着色器)。因此program更像是一個OC/swift和GLSL通訊的橋樑。 就像是JSCoreBridge的做用!!!
// renderDraw的代碼
// 一、連接program
glLinkProgram(self.myPrograme);
GLint linkStatus;
// 2.獲取連接狀態
glGetProgramiv(self.myPrograme, GL_LINK_STATUS, &linkStatus);
if (linkStatus == GL_FALSE) {
// 連接異常處理
return;
}
// 3.使用program
glUseProgram(self.myPrograme);
複製代碼
值得注意的是———— 到這一步爲止,咱們已經完成開始使用program,目的是爲了將頂點數據等其餘傳入頂點和片元着色器,因此咱們也能夠先作接下來的 設置頂點座標和紋理座標 和 設置頂點數據 ,只要在經過program將數據傳入着色器以前完成使用program就能夠。
// renderDraw的代碼
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,
};
複製代碼
attAr存放了紅色虛線分割的兩個三角形,共計六個頂點和紋理座標。 前3個是頂點座標,後邊2個是映射的紋理座標
這裏的座標系是物體座標系,即物體中心點是座標中心點,以下圖所示:左邊是按照數組映射了紋理的頂點座標系,右邊是紋理座標系。
補充: 咱們在貼圖的時候,紋理的座標系以左下角爲原點。長寬均爲1,和紋理圖片的實際寬高無關。咱們要作的就是將紋理對應的映射到頂點上,完成貼圖。
這一步,咱們的目的很簡單:就是申請一塊頂點緩存區存放剛剛的頂點數據,再將緩存區內的頂點經過program傳給頂點着色器。
又是緩存三步曲:申請glGenBuffers、綁定glBindBuffer、放數據glBufferData
// renderDraw的代碼
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);
複製代碼
// renderDraw的代碼
// 「position」是着色器中定義的變量名,先經過glGetAttribLocation獲取着色器中這個變量標誌,若是position是用uniform定義,則使用glGetUniformLocation
GLuint position = glGetAttribLocation(self.myPrograme, "position");
// 設置合適的格式從buffer裏面讀取數據
glEnableVertexAttribArray(position);
// 設置讀取方式
glVertexAttribPointer(position, 3, GL_FLOAT,GL_FALSE,sizeof(GLfloat) * 5, NULL);
複製代碼
這也是OC和着色器的數據傳遞的固定格式。其中的glVertexAttribPointer
//參數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 (GLuint indx, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* ptr)
複製代碼
// renderDraw的代碼
GLuint textCoor = glGetAttribLocation(self.myPrograme, "textCoordinate");
glEnableVertexAttribArray(textCoor);
glVertexAttribPointer(textCoor, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, (float *)NULL + 3);
複製代碼
咱們封裝一個方法setupTexture從圖片中獲取紋理
// renderDraw的代碼
[self setupTexture:@"bali"];
複製代碼
setupTexture方法展開:
- (GLuint)setupTexture:(NSString *)fileName {
//一、將 UIImage 轉換爲 CGImageRef
CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
//判斷圖片是否獲取成功
if (!spriteImage) {
NSLog(@"Failed to load image %@", fileName);
exit(1);
}
//二、讀取圖片的大小,寬和高
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);
//五、在CGContextRef上--> 將圖片繪製出來
/*
CGContextDrawImage 使用的是Core Graphics框架,座標系與UIKit 不同。UIKit框架的原點在屏幕的左上角,Core Graphics框架的原點在屏幕的左下角。
CGContextDrawImage
參數1:繪圖上下文
參數2:rect座標
參數3:繪製的圖片
*/
CGRect rect = CGRectMake(0, 0, width, height);
//6.使用默認方式繪製
CGContextDrawImage(spriteContext, rect, spriteImage);
//七、畫圖完畢就釋放上下文
CGContextRelease(spriteContext);
//八、綁定紋理到默認的紋理ID(
glBindTexture(GL_TEXTURE_2D, 0);
//9.設置紋理屬性
/*
參數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;
//10.載入紋理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);
//11.釋放spriteData
free(spriteData);
return 0;
}
複製代碼
glUniform1i(glGetUniformLocation(self.myPrograme, "colorMap"), 0);
glDrawArrays(GL_TRIANGLES, 0, 6);
[self.myContext presentRenderbuffer:GL_RENDERBUFFER];
代碼的核心在於文章的 渲染 一大段的分析內。
第一個點:緩存區的使用
// 一、申請標誌
glGenBuffers (GLsizei n, GLuint* buffers);
// 二、綁定標誌和緩存區
glBindBuffer (GLenum target, GLuint buffer);// 普通buffer綁定
glBindFramebuffer (GLenum target, GLuint framebuffer);//Framebuffer綁定
glBindRenderbuffer (GLenum target, GLuint renderbuffer);//Renderbuffer的綁定
複製代碼
第二個點:着色器的編譯和編寫
// 一、建立
glCreateShader (GLenum type);
// 二、附着GLSL源碼字符串
glShaderSource (GLuint shader, GLsizei count, const GLchar* const *string, const GLint* length);
// 三、編譯
glCompileShader (GLuint shader);
複製代碼
第三個點:program的建立和使用
// 一、建立
glCreateProgram (void) ;
// 二、附着着色器
glAttachShader (GLuint program, GLuint shader); // 附着頂點着色器
glAttachShader (GLuint program, GLuint shader); // 附着片元着色器
// 三、連接
glLinkProgram (GLuint program);
// 四、使用
glUseProgram (GLuint program);
複製代碼
第四個點:CPU與GPU的數據傳遞,經過program,將數據傳給着色器
// 一、從program中得到着色器的輸入變量
glGetAttribLocation; // 得到attribute定義的輸入變量
glGetUniformLocation;// 得到uniform定義的輸入變量
// 二、設置合適的格式從buffer裏面讀取數據
glEnableVertexAttribArray
// 三、設置讀取方式
glVertexAttribPointer;
複製代碼
第五個點:圖片的解碼 就是setupTexture方法
這個案例只是簡單以一張靜態圖片爲例。可是遺留了一個小問題:咱們在構造頂點數據時,Y軸正向是向上的,而通過一系列座標系轉換後,獲得iOS手機屏幕座標系截然相反,Y軸是向下的,最終呈現了圖片倒置的狀況。就須要對圖片作反轉處理,這個能夠在頂點着色器內完成,也能夠在OC代碼裏完成。