一、前言
OpenGL ES是OpenGL的一个子集,主要用于移动端图形渲染。苹果在2018年WWDC中宣布用metal代替OpenGL,据说渲染3D图形能提升10倍的性能。但是不管是OpenGL还是metal底层原理都是大同小异的。
这篇文章主要目的有两个:1.了解OpenGL ES加载纹理的流程,在之前的文章中已经实现了OpenGL加载纹理,这里用OpenGL ES;2.通过自定义顶点着色器、片元着色器对图片进行特殊处理,也就是我们常说的滤镜。
二、OpenGL ES加载纹理
整个流程分为五个步骤:
- 相关配置:CAEAGLLayer、EAGLContext、渲染缓冲区、帧缓冲区初始化
- 编译链接使用自定义着色器程序
- 解压缩图片并绑定到默认的纹理ID上
- 着色器程序处理纹理
- 渲染
1.配置
iOS为我们提供了CAEAGLLayer、EAGLContext。CAEAGLLayer相当于一个画布,EAGLContext是上下文,这个很重要,因为OpenGL是状态机,设置当前工作区为上下文,OpenGL才能知道你到底想将图形显示在哪里。配置的代码异常简单,请看:
@property (nonatomic, strong) EAGLContext *context;
@property (nonatomic, strong) CAEAGLLayer *myLayer;
@property (nonatomic, assign) GLuint renderBuffer;
@property (nonatomic, assign) GLuint frameBuffer;
- (void)setupLayer{
self.myLayer = [CAEAGLLayer layer];
self.myLayer.frame = self.bounds;
[self.myLayer setContentsScale:[UIScreen mainScreen].scale];
[self.layer addSublayer:self.myLayer];
self.myLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:
@false, kEAGLDrawablePropertyRetainedBacking,
kEAGLDrawablePropertyColorFormat, kEAGLColorFormatRGBA8, nil];
}
- (void)setupContent{
EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
if (!context) {
exit(1);
}
self.context = context;
BOOL result = [EAGLContext setCurrentContext:self.context];
if (!result) {
exit(1);
}
}
/// 清空渲染缓冲区和帧缓冲区
- (void)clearBuffer{
glDeleteRenderbuffers(1, &_renderBuffer);
_renderBuffer = 0;
glDeleteFramebuffers(1, &_frameBuffer);
_frameBuffer = 0;
}
/// 开辟缓冲区
- (void)setupBuffer{
glGenRenderbuffers(1, &_renderBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer);
glGenFramebuffers(1, &_frameBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer);
[self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myLayer];
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _frameBuffer);
}
2.编译链接使用自定义着色器程序
可编程管线中只有顶点着色器和片元着色器可以自定义。自定义着色器需要用到着色器语言GL Shader Language,一般简称为GLSL,它是跨平台的。然而GLSL没有专门的编译器,所以只能在项目中手动去编译链接。下面一起来看看GLSL的编译链接流程。
a.编译
/// 如果这里看不明白,继续往下来,将两个流程结合起来
+ (void)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)filePath{
// 读取文件中的字符串
NSString *content = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
const GLchar *source = (GLchar *)[content UTF8String];
// 创建shader
*shader = glCreateShader(type);
glShaderSource(*shader, 1, &source, NULL);
// 编译
glCompileShader(*shader);
}
b.链接
/// verFile顶点着色器文件,fragFile片元着色器文件
+ (GLuint)loadShaderProgramFrom:(NSString *)verFile fragFile:(NSString *)fragFile{
GLuint verShader, fragShader;
/// 创建着色器程序
GLuint program = glCreateProgram();
[GLSLUtils compileShader:&verShader type:GL_VERTEX_SHADER file:verFile];
[GLSLUtils compileShader:&fragShader type:GL_FRAGMENT_SHADER file:fragFile];
// 链接
glAttachShader(program, verShader);
glAttachShader(program, fragShader);
// 释放
glDeleteShader(verShader);
glDeleteShader(fragShader);
return program;
}
3.解压缩图片并绑定到默认的纹理ID上
解压缩图片用的是CoreGraphics框架,请看:
+ (void)readTexture:(NSString *)imgName{
// 判断是哪里的图片,目前先认为是asset中的
// 1.将UIimage转成 CGImageRef
CGImageRef spriteImage = [UIImage imageNamed:imgName].CGImage;
if (!spriteImage) {
NSLog(@"fail load image %@", imgName);
exit(1);
}
size_t width = CGImageGetWidth(spriteImage);
size_t height = CGImageGetHeight(spriteImage);
// 获取图片字节数 宽 x 高 x 4 (RGBA)
GLubyte *spriteData = (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 spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4, CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
// 在CGContextRef 上将图片绘制出来
CGRect rect = CGRectMake(0, 0, width, height);
CGContextDrawImage(spriteContext, rect, spriteImage);
CGContextRelease(spriteContext);
// 将纹理绑定到默认的纹理ID上
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);
float fw = width, fh = height;
// 载入纹理数据
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fw, fh, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
// 释放
free(spriteData);
}
4.着色器程序处理纹理
// 使用自定义着色器程序,position是顶点着色器中定义的变量
GLuint position = glGetAttribLocation(self.program, "position");
// 允许顶点着色器读取GPU(服务器端)数据
glEnableVertexAttribArray(position);
// 读取方式
/*
参数1:用什么着色器,这里是顶点着色器 position 顶点数据ID
参数2:每次读取字节数 数量
参数3:数据类型
参数4:是否希望数据被标准化(归一化),只表示方向不表示大小
参数5:步长(Stride),指定在连续的顶点属性之间的间隔
参数6:位置数据在缓冲区起始位置的偏移量
*/
glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, NULL);
// 片源着色器
GLuint vTextCoor = glGetAttribLocation(self.program, "vTextCoor");
glEnableVertexAttribArray(vTextCoor);
glVertexAttribPointer(vTextCoor, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, (GLfloat *)NULL+3);
这段代码是对顶点着色器和片元着色器分别进行传值。直接看这段代码,可能看不明白,因为没有来龙去脉,不过没有关系,继续看。
5.渲染
这里将显示完整的渲染过程,有些过程是被封装了的。例如,着色器程序的编译、链接过程。
- (void)render{
float scale = [UIScreen mainScreen].scale;
glViewport(0, 0, self.width*scale, self.height*scale);
glClearColor(0.5, 0.5, 0.5, 1);
glClear(GL_COLOR_BUFFER_BIT);
NSString *vFile = [[NSBundle mainBundle] pathForResource:@"normal" ofType:@"vsh"];
NSString *fFile = [[NSBundle mainBundle] pathForResource:@"normal" ofType:@"fsh"];
self.program = [GLSLUtils loadShaderProgramFrom:vFile fragFile:fFile];
// 链接
glLinkProgram(self.program);
GLint linkStatus;
glGetProgramiv(self.program, GL_LINK_STATUS, &linkStatus);
if (linkStatus == GL_FALSE) {
GLchar msg[512];
glGetProgramInfoLog(self.program, sizeof(msg), 0, &msg[0]);
NSString *info = [NSString stringWithUTF8String:msg];
NSLog(@"%@", info);
return;
}
NSLog(@"link success!");
glUseProgram(self.program);
float sub = 1.0;
// 设置顶点、纹理坐标
GLfloat points[] = {
-sub, -sub, 0, 0, 0,
sub, -sub, 0, 1, 0,
sub, sub, 0, 1, 1,
-sub, sub, 0, 0, 1,
};
// 开辟顶点缓冲区
GLuint vexBuffer;
glGenBuffers(1, &vexBuffer);
glBindBuffer(GL_ARRAY_BUFFER, vexBuffer);
// 第一个参数target可以为GL_ARRAY_BUFFER或GL_ELEMENT_ARRAY。
// 第二个参数size为待传递数据字节数量
// 第三个参数为源数据数组指针,如data为NULL,则VBO仅仅预留给定数据大小的内存空间。
// 最后一个参数usage标志位VBO的另一个性能提示,它提供缓存对象将如何使用:static、dynamic或stream、与read、copy或draw。
glBufferData(GL_ARRAY_BUFFER, sizeof(points), points, GL_STATIC_DRAW);
// 使用自定义着色器程序
GLuint position = glGetAttribLocation(self.program, "position");
// 允许顶点着色器读取GPU(服务器端)数据
glEnableVertexAttribArray(position);
// 读取方式
/*
参数1:用什么着色器,这里是顶点着色器 position 顶点数据ID
参数2:每次读取字节数 数量
参数3:数据类型
参数4:是否希望数据被标准化(归一化),只表示方向不表示大小
参数5:步长(Stride),指定在连续的顶点属性之间的间隔
参数6:位置数据在缓冲区起始位置的偏移量
*/
glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, NULL);
// 片源着色器
GLuint vTextCoor = glGetAttribLocation(self.program, "vTextCoor");
glEnableVertexAttribArray(vTextCoor);
glVertexAttribPointer(vTextCoor, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, (GLfloat *)NULL+3);
// 加载纹理
[GLSLUtils readTexture:@"girl"];
// 设置纹理采样器
glUniform1i(glGetUniformLocation(self.program, "colorMap"), 0);
// 绘图
glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
[self.context presentRenderbuffer:GL_RENDERBUFFER];
}
看到这里,你会发现没有顶点和片元着色器的源码,别急,下面就是:
/// normal.vsh
attribute vec4 position;
attribute vec2 vTextCoor;
varying lowp vec2 varyTextCoord;
void main() {
varyTextCoord = vTextCoor;
gl_Position = position;
}
//---------------------------------------------------------------//
/// normal.fsh
varying lowp vec2 varyTextCoord;
// 取色器
uniform sampler2D colorMap;
void main() {
// 取色器取色,varyTextCoord是从顶点着色器传进来的纹理坐标
lowp vec2 coor = vec2(varyTextCoord.x, 1.0-varyTextCoord.y);
gl_FragColor = texture2D(colorMap, coor);
}
使用自定义着色器程序加载纹理的流程大致如此,有些细节没有讲到,如果不理解可以在文章最后留言。如果到这里还不是很清楚,没关系,在文章末尾有完整的demo地址可供参考。
三、几种滤镜
GLSL是一种让初学者很难受的语言,它没有代码联想,也不会给你任何提示,即便你的语法有问题,同时它对精度和变量类型很严格。因为顶点着色器代码都一样,所以下面只提供片元着色器代码。
- 分屏滤镜
- 旋涡滤镜
- 马赛克滤镜
- 仿抖音灵魂出窍滤镜
- 灰度滤镜
1.分屏滤镜
看效果图就知道怎么分屏了
分屏
varying lowp vec2 varyTextCoord;
// 取色器
uniform sampler2D colorMap;
void main() {
// 取色器取色,varyTextCoord是从顶点着色器传进来的纹理坐标
lowp vec2 coor = vec2(varyTextCoord.x, 1.0-varyTextCoord.y);
if (coor.x < 0.5) {
coor.x = coor.x * 2.0;
}else{
coor.x = coor.x * 2.0 - 1.0;
}
if (coor.y < 0.5) {
coor.y = coor.y * 2.0;
}else{
coor.y = coor.y * 2.0 - 1.0;
}
gl_FragColor = texture2D(colorMap, coor);
}
// 分屏滤镜太简单了,不解释
2.旋涡滤镜
先看效果
旋涡
旋涡滤镜需要分两步:
- 第一步,设置旋转区域和角度,这里是将该正方形的内切圆整体旋转了90度,纹理坐标最初就是上下颠倒的,然后旋转取值实际上是逆时针,最终效果就是顺时针旋转90度了。
- 第二步,设置衰减因子,这个很关键,在glsl中有介绍,demo中的衰减是通过滑动条的值动态修改的。
// 定义该区域内所有的float的精度
precision mediump float;
uniform float yinzi;
varying vec2 varyTextCoord;
uniform sampler2D colorMap;
// 最大半径,在这个半径内都会发生旋转
const float maxRadius = 0.5;
// 偏移角度
const float angle = 90.0;
void main() {
vec2 xy = vec2(varyTextCoord.x-0.5, varyTextCoord.y-0.5);
float r = length(xy);
// 将平面坐标系转成极坐标系
// x = r*cos(a) y = r*sin(a)
// tana = y / x
// a = atan(y/x) = atan(y, x) 定义域(-00, +00),可取
// a = acos(x/r) = asin(y/x) 定义域(-1, 1),不可取
// float a = atan(xy.y , xy.x) + radians(angle);
// float a = asin(xy.x/r)
// float a = acos(xy.y/r)
if (r <= maxRadius) {
float sub = 1.0 - yinzi * r * r;
float a = atan(xy.y , xy.x) + radians(angle) * 2.0 * sub;
xy = vec2(0.5, 0.5) + vec2(r*cos(a), r*sin(a));
}else{
xy = varyTextCoord;
}
gl_FragColor = texture2D(colorMap, xy);
}
3.马赛克滤镜
看效果之前,简单说下马赛克的原理,用一句话解释:将某个区域类的颜色值用同一个颜色值替代。看下面的效果图。
马赛克
这里的马赛克效果比较简单不做过多解释。
precision mediump float;
varying vec2 varyTextCoord;
uniform sampler2D colorMap;
// 几行
uniform int rowCount;
// 几列
//uniform int clownCount;
void main() {
int clownCount = rowCount;
vec2 coor = vec2(varyTextCoord.x, 1.0-varyTextCoord.y);
float row = 1.0/float(clownCount);
float clown = 1.0/float(rowCount);
float x = coor.x;
float y = coor.y;
float currentX = row * float(int(x/row));
float currentY = clown * float(int(y/clown));
gl_FragColor = texture2D(colorMap, vec2(currentX, currentY));
}
4.仿抖音灵魂出窍滤镜
一句话解释原理:将纹理做缩放效果并和原纹理进行颜色混合计算出最终的颜色值。看效果图。
灵魂出窍
precision mediump float;
// 放大因子,从外面传入,通过定时器控制
uniform float scale;
uniform sampler2D colorMap;
varying vec2 varyTextCoord;
void main() {
vec2 coor = vec2(varyTextCoord.x, 1.0-varyTextCoord.y);
// 原图
vec4 originColor = texture2D(colorMap, coor);
// 放大的图
float x = coor.x - 0.5;
float y = coor.y - 0.5;
coor = vec2(coor.x - x * scale, coor.y - y * scale);
float mask = 0.3;
vec4 scaleColor = texture2D(colorMap, coor);
// vec4 endColor = vec4(originColor.r * (1.0-mask) + (scaleColor.r+0.5) * mask,
// originColor.g * (1.0-mask) + scaleColor.g * mask,
// originColor.b * (1.0-mask) + scaleColor.b * mask,
// originColor.a * (1.0-mask) + scaleColor.a * mask);
vec4 endColor = originColor * (1.0-mask) + scaleColor * mask;
gl_FragColor = endColor;
}
5.灰度滤镜
so easy,啥也不想讲
灰度
precision mediump float;
varying vec2 varyTextCoord;
uniform sampler2D colorMap;
void main() {
/*
任何颜色都有红、绿、蓝三原色组成,假如原来某点的颜色为RGB(R,G,B),那么,我们可以通过下面几种方法,将其转换为灰度:
1.浮点算法:Gray=R*0.3+G*0.59+B*0.11
2.整数方法:Gray=(R*30+G*59+B*11)/100
3.平均值法:Gray=(R+G+B)/3;
4.仅取绿色:Gray=G;
*/
vec2 coor = vec2(varyTextCoord.x, 1.0-varyTextCoord.y);
vec4 source = texture2D(colorMap, coor);
float r = (source.r + source.g + source.b)/3.0;
r = source.r*0.3 + source.g*0.59 + source.b*0.11;
r = dot(source, vec4(0.3, 0.59, 0.11, 0.0));
vec4 color = vec4(r, r, r, source.a);
gl_FragColor = color;
}
四、总结
这篇文章主要讲了OpenGL ES加载纹理的流程,还有几种简单滤镜。实际上直接看这篇文章应该还是不能理解,所以建议先看看前几篇,对OpenGL有了一些认识之后再看。
最后附上demo地址:https://github.com/zhaoguyixia/OpenGL.git
祝生活愉快!!








网友评论