OpenGL ES 案例:分屏滤镜

本案例最终实现的效果图如下(包括正常无分屏/2/3/4/6/9分屏)

实现一个正常无分屏的滤镜

需要使用 GLSL 自定义着色器(包括顶点着色器、片元着色器)

1. 实现自定义着色器

  • 顶点着色器
attribute vec4 Position;
attribute vec2 TextureCoords;
varying vec2 TextureCoordsVarying;

void main() {
    gl_Position = Position;
    TextureCoordsVarying = TextureCoords;
}
  • 片元着色器
precision highp float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;

void main() {
    vec4 mask = texture2D(Texture, TextureCoordsVarying);
    gl_FragColor = vec4(mask.rgb, 1.0);
}

2. 视图控制器

这里首先实现的是正常、无分屏效果的原始图片展示(无滤镜效果)。后续的分屏滤镜是在此基础上,改动片元着色器的源码实现。

滤镜初始化
  • 设置上下文&设置当前上下文
self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
[EAGLContext setCurrentContext:self.context];
  • 设置顶点数据&顶点缓存区
//开辟顶点数组内存空间
self.vertices = malloc(sizeof(SenceVertex) * 4);
    
//初始化顶点(0,1,2,3)的顶点坐标以及纹理坐标
self.vertices[0] = (SenceVertex){{-1, 1, 0}, {0, 1}};
self.vertices[1] = (SenceVertex){{-1, -1, 0}, {0, 0}};
self.vertices[2] = (SenceVertex){{1, 1, 0}, {1, 1}};
self.vertices[3] = (SenceVertex){{1, -1, 0}, {1, 0}};

//设置顶点缓冲区
GLuint vertexBuffer;
glGenBuffers(1, &vertexBuffer);
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
GLsizeiptr bufferSizeBytes = sizeof(SenceVertex) * 4;
glBufferData(GL_ARRAY_BUFFER, bufferSizeBytes, self.vertices, GL_STATIC_DRAW);
  • 创建 layer &绑定渲染、帧缓存区
//创建图层(CAEAGLLayer)
CAEAGLLayer *layer = [[CAEAGLLayer alloc] init];
//设置图层frame
layer.frame = CGRectMake(0, 100, self.view.frame.size.width, self.view.frame.size.width);
//设置图层的scale
layer.contentsScale = [[UIScreen mainScreen] scale];
//给view添加layer
[self.view.layer addSublayer:layer];

//渲染缓存区,帧缓存区对象
GLuint renderBuffer;
GLuint frameBuffer;

//获取帧渲染缓存区名称,绑定渲染缓存区以及将渲染缓存区与layer建立连接
glGenRenderbuffers(1, &renderBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, renderBuffer);
[self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer];

//获取帧缓存区名称,绑定帧缓存区以及将渲染缓存区附着到帧缓存区上
glGenFramebuffers(1, &frameBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderBuffer);
  • 图片解压缩&加载纹理
//获取处理的图片路径
NSString *imgPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"xiaochen.png"];
//读取图片
UIImage *img = [UIImage imageWithContentsOfFile:imgPath];

//将 UIImage 转化为 CGImageRef
CGImageRef cgImageRef = [img CGImage];

//获取图片的大小、宽高
GLuint width = (GLuint)CGImageGetWidth(cgImageRef);
GLuint height = (GLuint)CGImageGetHeight(cgImageRef);
//获取图片的rect
CGRect rect = CGRectMake(0, 0, width, height);
//获取图片的颜色空间
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();

//获取图片字节数 宽*高*4(RGBA)
void *spriteData = malloc(width*height*4);

//创建上下文
CGContextRef context = CGBitmapContextCreate(spriteData, width, height, 8, width * 4, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);

//将图片翻转过来(图片默认是倒置的)
CGContextTranslateCTM(context, 0, height);
CGContextScaleCTM(context, 1.0f, -1.0f);
CGColorSpaceRelease(colorSpace);
CGContextClearRect(context, rect);

//对图片进行重新绘制,得到一张新的解压缩后的位图
CGContextDrawImage(context, rect, cgImageRef);

//设置纹理属性
//获取纹理ID
GLuint textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);

//载入纹理2D数据
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);

//设置纹理属性
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

//绑定纹理
glBindTexture(GL_TEXTURE_2D, 0);

//释放context,spritedata
CGContextRelease(context);
free(spriteData);
  • 设置视口
glViewport(0, 0, self.drawableWidth, self.drawableHeight);
  • 调用默认着色器

1. 编译着色器

通过传入的 文件前缀 name,来获取当前选择的自定义 shader。

//获取 shader 路径
NSString *shaderPath = [[NSBundle mainBundle] pathForResource:name ofType:shaderType == GL_VERTEX_SHADER ? @"vsh" : @"fsh"];
NSString *shaderString = [NSString stringWithContentsOfFile:shaderPath encoding:NSUTF8StringEncoding error:nil];

//创建 shader->根据 shaderType
GLuint shader = glCreateShader(shaderType);

//获取 shader source
const char *shaderStringUTF8 = [shaderString UTF8String];
int shaderStringLength = (int)[shaderString length];
glShaderSource(shader, 1, &shaderStringUTF8, &shaderStringLength);

//编译 shader
glCompileShader(shader);

2. 获取着色器程序

通过调用上一步生成的着色器,将顶点、片元着色器附着到 program & 连接 program

//编译顶点/片元着色器
GLuint vertexShader = [self compileShaderWithName:shaderName type:GL_VERTEX_SHADER];
GLuint fragmentShader = [self compileShaderWithName:shaderName type:GL_FRAGMENT_SHADER];

//将顶点/片元附着到 program
GLuint program = glCreateProgram();
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);

//链接 program
glLinkProgram(program);

3. 数据传递、绑定纹理

调用第二部生成的 program,打开 Attrib 通道、数据传递、激活纹理、绑定纹理、设置纹理采样器等。

//获取着色器的 program
GLuint program = [self programWithShaderName:name];

//use program
glUseProgram(program);

//获取 Position、Texture、TextureCoords 的索引位置
GLuint positionSlot = glGetAttribLocation(program, "position");
GLuint textureSlot = glGetUniformLocation(program, "texture");
GLuint textureCoordsSlot = glGetAttribLocation(program, "textureCoords");

//激活纹理、绑定纹理
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, self.textureID);

//纹理 sample
glUniform1i(textureSlot, 0);

//打开positionSlot 属性并且传递数据到positionSlot中(顶点坐标)
glEnableVertexAttribArray(positionSlot);
glVertexAttribPointer(positionSlot, 3, GL_FLOAT, GL_FALSE, sizeof(SenceVertex), NULL + offsetof(SenceVertex, positionCoord));

//打开 textureCoordsSlot 属性并传递数据到 textureCoordsSlot(纹理坐标)
glEnableVertexAttribArray(textureCoordsSlot);
glVertexAttribPointer(textureCoordsSlot, 2, GL_FLOAT, GL_FALSE, sizeof(SenceVertex), NULL + offsetof(SenceVertex, textureCoord));
  • 渲染
// 清除画布
glClear(GL_COLOR_BUFFER_BIT);
glClearColor(1, 1, 1, 1);

//使用program
glUseProgram(self.program);
//绑定buffer
glBindBuffer(GL_ARRAY_BUFFER, self.vertexBuffer);

// 重绘
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
//渲染到屏幕上
[self.context presentRenderbuffer:GL_RENDERBUFFER];

实现效果图

以上部分的代码实现的是无分屏的原始图片,前面文章也有涉及,这里只展示了大概的逻辑,详细代码可查看完整代码的实现。

分屏滤镜

分屏滤镜的实现与无分屏的不同在于自定义的片元着色器源码的算法和不同,所以切换不同分屏效果主要需要改动片元着色器的部分代码。

  • 新建自定义的 shader 文件,顶点着色器文件无任何变化(可以共用顶点着色器文件),片元着色器增加相应的分屏算法
  • 视图控制器类增加滑动视图,以切换不同的滤镜效果(这里涉及到的OC 源码不再详细说明,详情参考完整代码),切换时传入相应的自定义着色器的文件名,编译、加载相应的着色器,重新渲染

二分屏

二分屏是上下两个都显示原图中间的一半(当然你也可以选择左右显示),这里以上下两部分讲解,因为纹理 x 的坐标没有发生改变,所以只需要判断纹理 y 坐标的变化。

  • 当 y 在 [0, 0.5] 范围时,屏幕的 (0,0) 坐标需要对应图片的 (0,0.25),所以 y = y+0.25
  • 当 y 在 [0.5, 1] 范围时,屏幕的 (0,0.5) 坐标需要对应图片的 (0,0.25),所以y = y-0.25

片元着色器中 main 函数主要变化如下

void main() {
    vec2 uv = textureCoordsVarying.xy;
    if (uv.y >= 0.0 && uv.y <= 0.5) {
        uv.y = uv.y + 0.25;
    }
    else {
        uv.y = uv.y - 0.25;
    }
    gl_FragColor = texture2D(texture, vec2(uv.x, uv.y));
}

三分屏

需要显示屏幕上中下三部分,这里是显示原始图片的中间的 1/3。同样图片纹理坐标的 x 值是没有任何变化的

  • 当 y 在 [0, 1/3] 范围时, y = y+1/3
  • 当 y 在 [1/3, 2/3] 范围时,y 不变
  • 当 y 在 [2/3, 1] 范围时,y = y-1/3

片元着色器中 main 函数主要变化如下

void main() {
    vec2 uv = textureCoordsVarying.xy;
    if (uv.y < 1.0/3.0) {
        uv.y = uv.y + 1.0/3.0;
    }
    else if (uv.y > 2.0/3.0) {
        uv.y = uv.y - 1.0/3.0;
    }
    
    gl_FragColor = texture2D(texture, uv);
}

四分屏

将屏幕分成四等份,显示缩略后的纹理图片,图片纹理坐标的 x 值和 y 值都是变化的,屏幕坐标需要与纹理坐标一一映射。

  • 当 x 在 [0, 0.5] 范围时,x = x*2
  • 当 x在 [0.5, 1] 范围时,x = (x-0.5)*2
  • 当 y 在 [0, 0.5] 范围时,y = y*2
  • 当 y 在 [0.5, 1] 范围时,y = (y-0.5)*2

片元着色器中 main 函数主要变化如下

void main() {
    vec2 uv = textureCoordsVarying.xy;
    if (uv.x <= 0.5) {
        uv.x = uv.x * 2.0;
    }
    else {
        uv.x = (uv.x - 0.5) * 2.0;
    }
    
    if (uv.y <= 0.5) {
        uv.y = uv.y * 2.0;
    }
    else {
        uv.y = (uv.y - 0.5) * 2.0;
    }
    
    gl_FragColor = texture2D(texture, uv);
}

六分屏

六分屏是二分屏加三分屏的演变,原理是上下是二分屏的效果,左右需要实现三分屏的效果。

  • 当 x 在 [0, 1/3] 范围时,x = x+1/3
  • 当 x 在 [1/3, 2/3] 范围时,x 不变
  • 当 x 在 [2/3, 1] 范围时,x = x-1/3
  • 当 y 在 [0, 0.5] 范围时,y = y+0.25
  • 当 y 在 [0.5, 1] 范围时,y = y-0.24

片元着色器中 main 函数主要变化如下

void main() {
    vec2 uv = textureCoordsVarying.xy;
    if (uv.x <= 1.0/3.0) {
        uv.x = uv.x + 1.0/3.0;
    }
    else if (uv.x >= 2.0/3.0) {
        uv.x = uv.x - 1.0/3.0;
    }
    
    if (uv.y <= 0.5) {
        uv.y = uv.y + 0.25;
    }
    else {
        uv.y = uv.y - 0.25;
    }
    
    gl_FragColor = texture2D(texture, uv);
}

九分屏

九分屏就是四分屏的升级版,将屏幕分成九等份,图片纹理坐标的 x 值和 y 值都是变化的,屏幕坐标需要与纹理坐标一一映射。

当 x 在 [0, 1/3] 范围时,x = x3
当 x 在 [1/3, 2/3] 范围时,x = (x-1/3)
3
当 x 在 [2/3, 1] 范围时,x = (x-2/3)3
当 y 在 [0, 1/3] 范围时,y= y
3
当 y 在 [1/3, 2/3] 范围时,y = (y-1/3)3
当 y在 [2/3, 1] 范围时,y = (y-2/3)
3

void main() {
    vec2 uv = textureCoordsVarying.xy;
    if (uv.x <= 1.0/3.0) {
        uv.x = uv.x * 3.0;
    }
    else if (uv.x >= 1.0/3.0 && uv.x <= 2.0/3.0) {
        uv.x = (uv.x - 1.0/3.0) * 3.0;
    }
    else {
        uv.x = (uv.x - 2.0/3.0) * 3.0;
    }
    
    if (uv.y <= 1.0/3.0) {
        uv.y = uv.y * 3.0;
    }
    else if (uv.y >= 1.0/3.0 && uv.y <= 2.0/3.0) {
        uv.y = (uv.y - 1.0/3.0) * 3.0;
    }
    else {
        uv.y = (uv.y - 2.0/3.0) * 3.0;
    }
    
    gl_FragColor = texture2D(texture, uv);
}

完整的代码见 Github 分屏滤镜

你可能感兴趣的:(OpenGL ES 案例:分屏滤镜)