案例:使用編譯鏈接自定義的着色器(shader),用簡單的 glsl 語言來實現頂點、片元着色器,繪制圖形並進行簡單的變換。
思路:
1.創建圖層
2.創建上下文
3.清空緩存區
4.設置 RenderBuffer
5.設置 FrameBuffer
6.開始繪制
一、准備工作
步驟 1~5
1 - (void)layoutSubviews { 2 3 // 1. 創建設置圖層 4 // 設置 layer 5 self.myEGLLayer = (CAEAGLLayer *)self.layer; 6 7 // 設置 scale 8 [self setContentScaleFactor:[[UIScreen mainScreen] scale]]; 9 10 // 設置屬性 11 /* 12 kEAGLDrawablePropertyRetainedBacking:繪圖表面顯示后,是否保留其內容。 13 kEAGLDrawablePropertyColorFormat:可繪制表面的內部顏色緩存區格式,這個key對應的值是一個NSString指定特定顏色緩存區對象。默認是kEAGLColorFormatRGBA8; 14 15 kEAGLColorFormatRGBA8:32位RGBA的顏色,4*8=32位 16 kEAGLColorFormatRGB565:16位RGB的顏色, 17 kEAGLColorFormatSRGBA8:sRGB代表了標准的紅、綠、藍,即CRT顯示器、LCD顯示器、投影機、打印機以及其他設備中色彩再現所使用的三個基本色素,sRGB的色彩空間基於獨立的色彩坐標,可以使色彩在不同的設備使用傳輸中對應於同一個色彩坐標體系,而不受這些設備各自具有的不同色彩坐標的影響。 18 */ 19 // self.myEGLLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking:@(NO),kEAGLDrawablePropertyColorFormat:kEAGLColorFormatRGBA8}; 20 self.myEGLLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:@false,kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatRGBA8,kEAGLDrawablePropertyColorFormat,nil]; 21 22 23 // 2. 設置上下文 24 self.myContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; 25 if (!self.myContext) { 26 NSLog(@"create context failed!"); 27 return; 28 } 29 BOOL isSetSuccess = [EAGLContext setCurrentContext:self.myContext]; 30 if (!isSetSuccess) { 31 return; 32 } 33 34 35 // 3. 清空緩沖區 36 glDeleteBuffers(1, &_myColorRenderBuffer); 37 self.myColorRenderBuffer = 0; 38 glDeleteBuffers(1, &_myColorFrameBuffer); 39 self.myColorFrameBuffer = 0; 40 41 42 // 4. 設置渲染緩沖區 renderBuffer 43 // 生成緩沖區 ID 44 GLuint rb; 45 glGenRenderbuffers(1, &rb); 46 self.myColorRenderBuffer = rb; 47 // 綁定緩沖區 48 glBindRenderbuffer(GL_RENDERBUFFER, self.myColorRenderBuffer); 49 50 // 綁到 context: contect 與 eagllayer綁定在一起 51 [self.myContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myEGLLayer]; 52 53 54 // 5. 設置幀緩沖區 FrameBuffer 55 glGenBuffers(1, &_myColorFrameBuffer); 56 glBindFramebuffer(GL_FRAMEBUFFER, self.myColorFrameBuffer); 57 58 // 渲染緩沖區 與 幀緩沖區綁在一起 59 /* 60 target: 61 attachment:將 renderBuffer 附着到frameBuffer的哪個附着點上 62 renderbuffertarget 63 renderbuffer 64 */ 65 // glFramebufferRenderbuffer(GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer) 66 glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myColorRenderBuffer); 67 68 // 開始繪制 69 [self renderLayer]; 70 71 }
二、開始繪制
1、首先獲取並使用 鏈接后着⾊器對象,過程:
a1、創建⼀個頂點着⾊器對象和⼀個⽚段着⾊器對象
b1、將源代碼鏈接到每個着⾊器對象
c1、編譯着⾊器對象
d1、創建⼀個程序對象
e1、將編譯后的着⾊器對象連接到程序對象
f1、鏈接程序對象
a1、着色器:
頂點着色器代碼:
attribute vec4 position;
attribute vec2 textCoordinate;
varying lowp vec2 varyTextCoord;
void main() { varyTextCoord = textCoordinate; gl_Position = position; }
片元着色器代碼:
precision highp float;
varying lowp vec2 varyTextCoord;
uniform sampler2D colorMap;
void main() { gl_FragColor = texture2D(colorMap, varyTextCoord); }
注意:使用 .vsh / .fsh 區分頂點、片元着色器 --> .vsh --> 頂點着色器 / .fsh --> 片元着色器 --> .vsh / .fsh 文件,只是用來給開發者區分着色器代碼。其本質是一串字符串。
b1 ~ f1 過程:
1 - (void)renderLayer { 2 3 glClearColor(0.7, 0.7, 0.7, 1); 4 glClear(GL_COLOR_BUFFER_BIT);// 清空顏色緩沖區 5 6 /// 1. 設置視口 7 CGFloat mainScale = [UIScreen mainScreen].scale; 8 glViewport(self.frame.origin.x * mainScale, self.frame.origin.y * mainScale, self.frame.size.width * mainScale, self.frame.size.height * mainScale); 9 10 /// 2. 讀取着色器代碼 11 // 路徑 12 NSString *verPath = [[NSBundle mainBundle] pathForResource:@"shaderv" ofType:@"vsh"]; 13 NSString *fragPath = [[NSBundle mainBundle] pathForResource:@"shaderf" ofType:@"fsh"]; 14 15 /// 3. 加載着色器 16 self.myProgram = [self loadShadersWithVertex:verPath Withfrag:fragPath]; 17 18 /// 4. 鏈接 program 19 glLinkProgram(self.myProgram); 20 // 獲取連接狀態 21 GLint linkStatus; 22 glGetProgramiv(self.myProgram, GL_LINK_STATUS, &linkStatus); 23 if (linkStatus == GL_FALSE) {// 鏈接出錯 24 // 獲取錯誤信息 log 25 GLchar message[512]; 26 glGetProgramInfoLog(self.myProgram, sizeof(message), 0, &message[0]); 27 NSString *messageString = [NSString stringWithUTF8String:message]; 28 NSLog(@"Program Link Error:%@",messageString); 29 return; 30 } 31 32 /// 5. 使用 program 33 glUseProgram(self.myProgram); 34 }
加載編譯着色器:
1 // 加載着色器 2 // 頂點着色器 和 片元着色器 的代碼傳進來(.vsh .fsh) 3 -(GLuint)loadShadersWithVertex:(NSString *)vert Withfrag:(NSString *)frag { 4 5 // 1.定義 着色器 6 GLuint verShader, fragShader; 7 8 // 2.創建程序 program 9 GLint program = glCreateProgram();// 創建一個空的程序對象 10 11 // 3.編譯着色器 --> 封裝一個方法 compileShaderWithShader: 12 [self compileShaderWithShader:&verShader shaderType:GL_VERTEX_SHADER filePath:vert]; 13 [self compileShaderWithShader:&fragShader shaderType:GL_FRAGMENT_SHADER filePath:frag]; 14 15 // 4.attach shader, 將shader附着到 程序 16 glAttachShader(program, verShader); 17 glAttachShader(program, fragShader); 18 19 //5.已附着好的 shader 刪掉,避免不必要的內存占用 20 glDeleteShader(verShader); 21 glDeleteShader(fragShader); 22 23 return program;// 返回編譯好的程序 24 } 25 // 編譯着色器 26 /* 27 shader: 着色器 ID 28 type: 着色器類型 29 path: 着色器代碼文件路徑 30 */ 31 - (void)compileShaderWithShader:(GLuint *)shader shaderType:(GLenum)type filePath:(NSString *)path { 32 33 // 1.讀取文件路徑 34 NSString *file = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil]; 35 // NSString 轉 C 的 char 36 const GLchar *source = (GLchar *)[file UTF8String]; 37 38 // 2.創建對應類型的shader 39 *shader = glCreateShader(type); 40 41 // 3.讀取着色器源碼 將其附着到着色器對象上面 42 /* params: 43 shader: 要編譯的着色器對象 *shader 44 numOfStrings: 傳遞的源碼字符串數量 1個 45 參數3:strings: 着色器程序的源碼(真正的着色器程序源碼) 46 參數4:lenOfStrings: 長度,具有每個字符串長度的數組,或NULL,這意味着字符串是NULL終止的 47 */ 48 // glShaderSource(GLuint shader, GLsizei count, const GLchar *const *string, const GLint *length) 49 glShaderSource(*shader, 1, &source,NULL); 50 51 // 4. 編譯 52 glCompileShader(*shader); 53 }
2、繪制型關信息的設置,過程:
a2、設置坐標
b2、加載紋理
c2、draw 繪制
a2、設置坐標信息:
/// 6. 設置頂點、紋理坐標 // 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, }; /// 7. copy 到頂點緩沖區 GLuint buffer; glGenBuffers(1, &buffer); glBindBuffer(GL_ARRAY_BUFFER, buffer); // 頂點數據 copy 到緩沖區 glBufferData(GL_ARRAY_BUFFER, sizeof(attrArr), attrArr, GL_DYNAMIC_DRAW); /// 8. 打開通道 傳遞數據 // 8.1 頂點數據 // 獲取通道 ID /* glGetAttribLocation(GLuint program, const GLchar *name) program: name: 給誰傳 --> .vsh 的 position */ GLuint position = glGetAttribLocation(self.myProgram, "position"); // 打開通道 glEnableVertexAttribArray(position); // 讀數據 glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, NULL); // 8.2 紋理數據 /*傳給誰 --> .vsh 的 textCoordinate */ GLuint texture = glGetAttribLocation(self.myProgram, "textCoordinate"); glEnableVertexAttribArray(texture); glVertexAttribPointer(texture, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (float *)NULL + 3); /// 9. 加載紋理 [self loadTexture];
b2、加載紋理:
// 加載紋理 - (void)loadTexture { // 9.0 image 轉為 CGImageRef CGImageRef spriteImage = [UIImage imageNamed:@"0002"].CGImage; // 圖片是否獲取成功 if (!spriteImage) { NSLog(@"Failed to load image "); exit(1);// 退出程序 } // 獲取圖片寬高 size_t width = CGImageGetWidth(spriteImage); size_t height = CGImageGetHeight(spriteImage); // 獲取圖片字節數 寬*高*4(RGBA) GLubyte *spriteData = (GLubyte *) calloc(width * height * 4, sizeof(GLubyte)); // 創建上下文 /* data:指向要渲染的繪制圖像的內存地址 width:bitmap 的寬度,單位為像素 height:bitmap 的高度,單位為像素 bitPerComponent:內存中像素的每個組件的位數,比如 32 位 RGBA,就設置為 8 bytesPerRow:bitmap 的沒一行的內存所占的比特數 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(CGContextRef _Nullable c, CGRect rect, CGImageRef _Nullable image) c:繪圖上下文 rect:rect坐標 image:繪制的圖片 */ CGRect rect = CGRectMake(0, 0, width, height); CGContextDrawImage(spriteContext, rect, spriteImage); // 繪完 釋放上下文 CGContextRelease(spriteContext); // 9.1. 綁定紋理到默認的紋理ID glBindTexture(GL_TEXTURE_2D, 0); // 9.2. 設置紋理屬性 /* glTexParameteri(GLenum target, GLenum pname, GLint param) target:紋理維度 pname:線性過濾; 為s,t坐標設置模式 param: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); // 9.3 載入紋理 /* 載入紋理 glTexImage2D 參數1:紋理維度,GL_TEXTURE_2D 參數2:mip貼圖層次 參數3:紋理單元存儲的顏色成分(從讀取像素圖中獲得) 參數4:加載紋理寬度 參數5:加載紋理的高度 參數6:為紋理貼圖指定一個邊界寬度 0 參數7、8:像素數據的數據類型, GL_UNSIGNED_BYTE無符號整型 參數9:指向紋理圖像數據的指針 */ float fw = width, fh = height; glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fw, fh, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData); // 9.4 釋放 sprite free(spriteData); }
c2、繪制:
/// 10. 設置紋理采樣器 glUniform1i(glGetUniformLocation(self.myProgram, "colorMap"), 0); /// 11. 繪制 glDrawArrays(GL_TRIANGLES, 0, 6); // 12. 從渲染緩沖區顯示到屏幕 [self.myContext presentRenderbuffer:GL_RENDERBUFFER];
運行結果如下圖:
圖片是倒立的?如何解決呢?
UIKit 框架的原點是屏幕的左上角,Core Graphics 框架的原點是屏幕的左下角。
三、解決圖片倒立問題
1、加載紋理的繪制圖片過程中,將圖片通過轉換矩陣旋轉
// 矩陣轉換 - 翻轉圖片 // x、y 軸平移 CGContextTranslateCTM(spriteContext, rect.origin.x, rect.origin.y); // y 平移 CGContextTranslateCTM(spriteContext, 0, rect.size.height); // Y 軸方向 Scale -1 翻轉 CGContextScaleCTM(spriteContext, 1.0, -1.0); // 平移回原點位置處 CGContextTranslateCTM(spriteContext, -rect.origin.x, -rect.origin.y); // 重繪 CGContextDrawImage(spriteContext, rect, spriteImage);
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, -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, };
3、修改着色器代碼進行翻轉
3.1、紋理着色器中 對紋理 y 坐標進行翻轉
紋理着色器代碼:
varying lowp vec2 varyTextCoord; uniform sampler2D colorMap; void main() { //gl_FragColor = texture2D(colorMap, varyTextCoord); gl_FragColor = texture2D(colorMap, vec2(varyTextCoord.x,1.0-varyTextCoord.y)); }
3.2、頂點着色器中 對紋理坐標轉換
原理同 3.1
代碼:
attribute vec4 position; attribute vec2 textCoordinate; varying lowp vec2 varyTextCoord; void main() { //varyTextCoord = textCoordinate; varyTextCoord = vec2(textCoordinate.x,1.0-textCoordinate.y); gl_Position = position; }
3.3、頂點着色器 傳入旋轉矩陣對頂點進行旋轉
頂點着色器代碼:
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; }
繪制前 旋轉矩陣 --> 矩陣使用 uniform 修飾傳遞
// 1. rotate等於shaderv.vsh中的 uniform 屬性,rotateMatrix GLuint rotate = glGetUniformLocation(self.myPrograme, "rotateMatrix"); // 2.獲取渲旋轉的弧度 float radians = 180 * 3.14159f / 180.0f; // 3.求得弧度對於的sin\cos值 float s = sin(radians); float c = cos(radians); // 4.因為在3D課程中用的是橫向量,在OpenGL ES用的是列向量 // 參考Z軸旋轉矩陣 GLfloat zRotation[16] = { c,-s,0,0, s,c,0,0, 0,0,1,0, 0,0,0,1 }; // 5.設置旋轉矩陣 /* glUniformMatrix4fv (GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) location : 對於shader 中的ID count : 個數 transpose : 是否轉置矩陣 value : 指針 */ glUniformMatrix4fv(rotate, 1, GL_FALSE, zRotation);
一般使用第一種方式,修改着色器代碼 每一個像素都要運行一次,成本太高。