在iOS开发中我们都知道使用UIImageView来加载图片,本文主要是通过自定义顶点与片元着色器来实现图片的加载。
主要步骤
-
设置CAEAGLLayer
-
设置上下文
-
清空缓冲区
-
设置帧缓存区
-
设置渲染缓冲区
-
顶点与片元着色器的编写
-
绘制图片
以上步骤中除了6以外均在自定义view中实现
工程设置
自定义一个ESVIew类继承于UIView
在VIewController的ViewDIdLoad方法中将self.view强转成ESView,必须注意的是如果你的ViewController是Main.storyborad的,则必须在storyborad中设置view继承于ESView,否则强转不会成功。
设置CAEAGLLayer
将self.layer设置成CAEAGLLayer,并重写类方法layerClass,代码如下
/// 返回CAEAGLLayer类
+(Class)layerClass {
return [CAEAGLLayer class];
}
-(void)setUpLayer {
self.myEaglLayer = (CAEAGLLayer *)self.layer;
//设置分辨率
[self setContentScaleFactor:[UIScreen mainScreen].scale];
//设置描述属性
self.myEaglLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:@false,kEAGLDrawablePropertyRetainedBacking,kEAGLColorFormatRGBA8,kEAGLDrawablePropertyColorFormat, nil];
}
设置context
创建并设置当前view的上下文
-(void)setUpContext {
self.myContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
if (!self.myContext) {
NSLog(@"创建上下文失败");
return;
}
//设置上下文
if (![EAGLContext setCurrentContext:self.myContext]) {
NSLog(@"设置上下文失败");
return;
}
}
清空缓存区
清空帧缓存区以及渲染缓冲区
-(void)clearRenderBufferWithFramBuffer {
glDeleteBuffers(1, &_myColorRenderBuffer);
self.myColorRenderBuffer = 0;
glDeleteBuffers(1, &_myColorFrameBuffer);
self.myColorFrameBuffer = 0;
}
设置帧缓存
-(void)setRenderBuffer {
//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.myEaglLayer];
}
设置渲染缓存
-(void)setFrameBuffer {
//1.定义一个缓存区ID
GLuint buffer;
//2.申请一个缓存区标志
glGenBuffers(1, &buffer);
//3.
self.myColorFrameBuffer = buffer;
//4.
glBindFramebuffer(GL_FRAMEBUFFER, self.myColorFrameBuffer);
/*生成帧缓存区之后,则需要将renderbuffer跟framebuffer进行绑定,
调用glFramebufferRenderbuffer函数进行绑定到对应的附着点上,后面的绘制才能起作用
*/
//5.将渲染缓存区myColorRenderBuffer 通过glFramebufferRenderbuffer函数绑定到 GL_COLOR_ATTACHMENT0上。
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myColorRenderBuffer);
}
绘制(重点+难点)
绘制是我们使用着色器显示图片的重点和难点,在绘制中我们主要分为以下几个步骤
-
设置视图窗口
-
根据顶点与片元着色器文件生成program并连接program以及检测是否连接成功
-
设置图片的顶点与纹理坐标
-
读取顶点着色器中position,将顶点数据从内存copy到显存用来绘制图片位置
-
读取片元着色器的纹理信息,将纹理数据从内存copy到显存中用来绘制纹理图片
-
加载图片,用来解析图片
-
设置纹理采样,并通过三角线将图片绘制到屏幕上
设置窗口视图
glClearColor(1, 1, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
//设置窗口大小
CGFloat scale = [[UIScreen mainScreen] scale];
CGRect frame = self.frame;
//此处*scale是获取像素的大小
glViewport(frame.origin.x, frame.origin.y, frame.size.width * scale, frame.size.height * scale);
生成program并连接program
//获取顶点着色器以及片元着色器路径
NSString *renderPath = [[NSBundle mainBundle] pathForResource:@"shaderv" ofType:@"vsh"];
NSString *framePath = [[NSBundle mainBundle] pathForResource:@"shaderf" ofType:@"fsh"];
//设置program
self.myProgram = [self loadShader:renderPath frag:framePath];
//连接program
glLinkProgram(self.myProgram);
//连接状态
GLint linkState;
//获取连接状态
glGetProgramiv(self.myProgram, GL_LINK_STATUS, &linkState);
if (linkState == GL_FALSE) {
GLchar error[512];
glGetProgramInfoLog(self.myProgram, sizeof(error), 0, &error[0]);
NSString *message = [NSString stringWithUTF8String:error];
NSLog(@"连接失败了:%@\n",message);
return;
}
//使用program
glUseProgram(self.myProgram);
实现生成program方法
//加载buffer
-(GLuint)loadShader:(NSString *)vert frag:(NSString *)frag {
GLuint renderShader,frameShader;
GLuint program = glCreateProgram();
[self compileShader:&renderShader bufferType:GL_VERTEX_SHADER path:vert];
[self compileShader:&frameShader bufferType:GL_FRAGMENT_SHADER path:frag];
//连接着色器
glAttachShader(program, renderShader);
glAttachShader(program, frameShader);
//删除不用的着色器(释放空间)
glDeleteShader(renderShader);
glDeleteShader(frameShader);
return program;
}
//编译着色器代码
-(void)compileShader:(GLuint *)shader bufferType:(GLenum)type path:(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);
}
设置顶点与纹理坐标
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,//右下
};
顶点坐标处理
GLuint bufferId;
//申请顶点缓冲区标识符
glGenBuffers(1, &bufferId);
//绑定顶点缓冲区
glBindBuffer(GL_ARRAY_BUFFER, bufferId);
//把顶点数据copy到显存
glBufferData(GL_ARRAY_BUFFER, sizeof(attrArr), attrArr, GL_DYNAMIC_DRAW);
//通过program获得顶点着色器代码中的postion,注意postion必须与顶点着色器的一致
GLuint postion = glGetAttribLocation(self.myProgram, "position");
//读取数据
glEnableVertexAttribArray(postion);
glVertexAttribPointer(postion, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, NULL);
纹理坐标处理
//纹理数据
//通过program获得片元着色器的,注意textCoordinate必须与片元着色器的一致
GLuint textCoor = glGetAttribLocation(self.myProgram, "textCoordinate");
glEnableVertexAttribArray(textCoor);
glVertexAttribPointer(textCoor, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, (float*)NULL + 3);
图片数据解析
-(void)loadImageWithImageName:(NSString *)imageName {
CGImageRef refImage = [UIImage imageNamed:imageName].CGImage;
if (!refImage) {
NSLog(@"转换图片失败");
return;
}
//获得图片的大小
size_t width = CGImageGetWidth(refImage);
size_t height = CGImageGetHeight(refImage);
//获取图片的data
GLubyte *data = (GLubyte *) calloc(width * height * 4, sizeof(GLubyte));
//绘制上下文
/*
参数1:data,指向要渲染的绘制图像的内存地址
参数2:width,bitmap的宽度,单位为像素
参数3:height,bitmap的高度,单位为像素
参数4:bitPerComponent,内存中像素的每个组件的位数,比如32位RGBA,就设置为8
参数5:bytesPerRow,bitmap的每一行的内存所占的比特数
参数6:colorSpace,bitmap上使用的颜色空间 kCGImageAlphaPremultipliedLast:RGBA
*/
CGContextRef imageContextRef = CGBitmapContextCreate(data, width, height, 8, width * 4, CGImageGetColorSpace(refImage), kCGImageAlphaPremultipliedLast);
//在CGContextRef上--> 将图片绘制出来
/*
CGContextDrawImage 使用的是Core Graphics框架,坐标系与UIKit 不一样。UIKit框架的原点在屏幕的左上角,Core Graphics框架的原点在屏幕的左下角。
CGContextDrawImage
参数1:绘图上下文
参数2:rect坐标
参数3:绘制的图片
*/
CGRect frame = CGRectMake(0, 0, width, height);
//翻转图片
// [self rateImage:imageContextRef frame:frame];
//使用默认方式绘制
CGContextDrawImage(imageContextRef, frame, refImage);
//释放上下文
CGContextRelease(imageContextRef);
//绑定纹理
glBindTexture(GL_TEXTURE_2D, 0);
//设置纹理属性
//设置线性过滤
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);
//载入纹理数据
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (float)width, (float)height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
//释放data
free(data);
}
设置纹理采样以及绘制图片到屏幕
//设置纹理采样
glUniform1i(glGetUniformLocation(self.myProgram, "colorMap"), 0);
//使用三角形绘图
glDrawArrays(GL_TRIANGLES, 0, 6);
//渲染到屏幕上
[self.myContext presentRenderbuffer:GL_RENDERBUFFER];
顶点着色器代码编写
按照以上步骤我们生成一个空文件,代码实现如下
attribute vec4 position;
attribute vec2 textCoordinate;
varying lowp vec2 varyTextCoord;
void main()
{
// varyTextCoord = vec2(textCoordinate.x, 1.0 - textCoordinate.y);
varyTextCoord = textCoordinate;
gl_Position = position;
}
代码解释:position是处理顶点数据,textCoordinate处理的是纹理数据,varyTextCoord是将顶点着色器的纹理数据传输到片元着色器,如main中的varyTextCoord = textCoordinate;
片元着色器编写
创建文件步骤如顶点着色器文件,只是命名不一样,代码实现为
precision highp float;
varying lowp vec2 varyTextCoord;
uniform sampler2D colorMap;
void main()
{
//lowp vec4 temp = texture2D(colorMap, varyTextCoord);
//gl_FragColor = temp;
// gl_FragColor = texture2D(colorMap, vec2(varyTextCoord.x, 1.0 - varyTextCoord.y));
gl_FragColor = texture2D(colorMap, varyTextCoord);
}
代码解释:varyTextCoord保存了从顶点着色器传过来的纹理数据,注意这个varyTextCoord一定要与顶点着色器的一致;colorMap是用来纹理采样的。
顶点着色器与纹理着色器的最终结果如下
其中.fsh是片元着色器,.vsh是顶点着色器
实现效果
我们发现实现的图片是倒着的,那么我们怎么实现正立的图片呢?
图片翻转实现
方法一:将图片顶点旋转,纹理坐标不变
GLuint rotate = glGetUniformLocation(self.myProgram, "rotateMatrix");
float radians = 180 * 3.14159f / 180.0f;
float s = sin(radians);
float c = cos(radians);
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]);
方法二:解析图片时将图片翻转
#pragma mark - 绘制图片时翻转
-(void)rateImage:(CGContextRef)imageRefContext frame:(CGRect)rect {
CGContextTranslateCTM(imageRefContext, rect.origin.x, rect.origin.y);
CGContextTranslateCTM(imageRefContext, 0, rect.size.height);
CGContextScaleCTM(imageRefContext, 1.0, -1.0);
CGContextTranslateCTM(imageRefContext, -rect.origin.x, -rect.origin.y);
}
方案三:修改顶点着色器
在顶点着色器中翻转顶点坐标
void main()
{
varyTextCoord = vec2(textCoordinate.x, 1.0 - textCoordinate.y);
// varyTextCoord = textCoordinate;
gl_Position = position;
}
方法四:在片元着色器中翻转纹理(不推荐使用)
该方法不推荐使用,这个执行次数是根据图片的像素多少决定的,每个像素点都会执行一次
void main()
{
//lowp vec4 temp = texture2D(colorMap, varyTextCoord);
//gl_FragColor = temp;
gl_FragColor = texture2D(colorMap, vec2(varyTextCoord.x, 1.0 - varyTextCoord.y));
// gl_FragColor = texture2D(colorMap, varyTextCoord);
}
方法五:在设置顶点的时候翻转顶点坐标
//通过修改纹理坐标旋转图片(s不动,t取反加1)
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,//右下
};