OpenGL 十 - 002、GLSL案例-纹理图片绘制与翻转
案例:使用编译链接自定义的着色器(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);
一般使用第一种方式,修改着色器代码 每一个像素都要运行一次,成本太高。