OpenGL
OpenGL到底是什么。一般它被认为是一个API(Application Programming Interface, 应用程序编程接口),包含了一系列可以操作图形、图像的函数。然而,OpenGL本身并不是一个API,它仅仅是一个由Khronos组织制定并维护的规范(Specification)。
OpenGL怎么工作
OpenGL被当作客户端-服务器系统来实现的,应用程序是客户端,图形硬件厂商提供的OpenGL实现是服务器。客户端程序需要调用OpenGL的接口实现3D渲染,那么OpenGL命令和数据会缓存在内存中,在一定条件下,会将这些命令和数据通过CPU时钟发送到显存,在GPU的控制下,使用显存中的数据和命令,经过渲染管道完成图形的渲染,并将结果存入帧缓冲区中,帧缓冲区中的帧最终会被发送到显示器上,显示出结果。
渲染管道示意图
1、顶点处理:把一个单独的顶点输入顶点着色器,顶点着色器主要的目的是把3D坐标转为另一种3D坐标,OpenGL仅当3D坐标在3个轴(x、y和z)上都为-1.0到1.0的范围内时才处理它。所有在所谓的标准化设备坐标(Normalized Device Coordinates)范围内的坐标才会最终呈现在屏幕上(在这个范围以外的坐标都不会显示)。
2、图元装配:以顶点着色器的输出的顶点作为输入,装配成对应的图元(OpenGL ES 只支持三种图元,分别是顶点、线段、三角形,复杂的图形得通过渲染多个三角形来实现)。
3、处理图元:把上一步输出的图元输入到几何着色器,几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。
4、光栅化:把图元映射为最终屏幕上相应的像素,会裁剪超出视图外的像素(为了提高效率),最终生成片段。
5、处理片段:将片段输入到片段着色器,片段着色器的主要目的是计算一个像素的最终颜色(滤镜处理的地方)。
6、测试及混合:这个阶段检测片段的对应的深度值和模板值,用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。
小概念解析
OpenGL上下文:OpenGL自身是一个巨大的状态机,一系列的变量描述OpenGL此刻应当如何运行,OpenGL的状态通常被称为OpenGL上下文,我们通常使用如下途径去更改OpenGL状态:设置选项,操作缓冲,最后,我们使用当前OpenGL上下文来渲染。
顶点数据:渲染图像需要的顶点坐标数组,例如渲染一个三角形需要三个顶点。
着色器:在GPU上运行的小程序,在图形渲染管线中快速处理你的数据,有三种类型:顶点着色器、几何着色器、片段着色器,我们只需要配置顶点和片段着色器就行了,几何着色器是可选的,通常使用它默认的着色器就行了。
纹理:纹理是一个2D图片(甚至也有1D和3D的纹理),它可以用来添加物体的细节。
帧缓冲:一个接收渲染结果的缓冲区,为 GPU 指定存储渲染结果的区域(纹理或渲染缓冲区),默认的帧缓冲是在创建渲染窗口的时候生成和配置的,我们可以自定义自己的帧缓冲。
OpenGL坐标:范围是 -1 ~ 1,是一个三维的坐标系,通常用 X、Y、Z 来表示。Z 轴的正方向指向屏幕外。
纹理坐标:纹理坐标系的范围是 0 ~ 1,是一个二维坐标系,原点左下,用来标明该从图像的哪个部分采样(使用纹理坐标获取纹理颜色)。
光栅化:决定哪些像素被集合图元覆盖的过程。
OpenGL编程
普通软件开发,属于CPU编程,CPU编程是串行编程,跟着代码顺序执行,代码到哪就执行到哪。而OpenGL编程,属于GPU编程,一系列的变量描述OpenGL此刻应当如何运行。
编程核心:顶点着色器、片段着色器
编程流
创建OpenGL上下文、渲染图层——>创建帧缓冲、渲染缓冲并绑定——>创建着色器程序,加载着色器代码并编译、链接——>配置窗口大小,启动着色器,加载顶点数据和纹理——>绘制渲染——>数据清理
抖音滤镜实践
为了将上面的内容更好的串联起来,今天我们来实践一个经典的抖音滤镜——抖动。
预准备
可以从github.com/caixindong/… 下载我们的预备工程,该工程提供最基本的相机捕获工程,打开/XDCaptureService/Example/XDCaptureService/XDViewController.m文件,这里就是我们书写相关实践代码的地方:
- (void)captureService:(XDCaptureService *)service getPreviewLayer:(AVCaptureVideoPreviewLayer *)previewLayer {
if (previewLayer) {
dispatch_async(dispatch_get_main_queue(), ^{
[_contentView.layer addSublayer:previewLayer];
previewLayer.frame = _contentView.bounds;
});
}
}
- (void)captureService:(XDCaptureService *)service outputSampleBuffer:(CMSampleBufferRef)sampleBuffer {
}
我们先将- (void)captureService:(XDCaptureService *)service getPreviewLayer:(AVCaptureVideoPreviewLayer *)previewLayer里的代码先注释掉,我们不使用AVFoundation默认提供的AVCaptureVideoPreviewLayer作为我们的预览视图,因为默认的预览视图不支持我们做进一步的滤镜处理,所以我们需要自己实现一个基于OpenGL实现的预览视图,以支持我们从渲染层面对每一个视频帧做滤镜处理。
- (void)captureService:(XDCaptureService *)service outputSampleBuffer:(CMSampleBufferRef)sampleBuffer这个方法用于回调相机捕获的视频帧数据,我们需要在里面做视频帧渲染相关逻辑。
OpenGL实战
我们先新建一个XDOpenGLPreView类作为我们OpenGL预览视图,提供一个方法用于渲染外部传进来的视频帧数据:
#import <UIKit/UIKit.h>
#import <CoreVideo/CoreVideo.h>
@interface XDOpenGLPreView : UIView
- (void)renderPixelBuffer:(CVPixelBufferRef)pixelbuffer;
@end
1、创建OpenGL上下文、渲染图层
+ (Class)layerClass {
return [CAEAGLLayer class];
}
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000
if ( [UIScreen instancesRespondToSelector:@selector(nativeScale)] )
{
self.contentScaleFactor = [UIScreen mainScreen].nativeScale;
}
else
#endif
{
self.contentScaleFactor = [UIScreen mainScreen].scale;
}
CAEAGLLayer *eaglLayer = (CAEAGLLayer *)self.layer;
eaglLayer.opaque = YES;
eaglLayer.drawableProperties = @{ kEAGLDrawablePropertyRetainedBacking : @(NO),
kEAGLDrawablePropertyColorFormat : kEAGLColorFormatRGBA8 };
_oglContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
if ( ! _oglContext ) {
NSLog( @"Problem with OpenGL context." );
return nil;
}
}
return self;
}
CAEAGLLayer是CALayer的一个子类,用来显示任意的OpenGL图形,他作为我们的渲染图层。
kEAGLDrawablePropertyRetainedBacking :NO:配置渲染图层保留任何以前绘制的图像留作以后重用;
kEAGLDrawablePropertyColorFormat :kEAGLColorFormatRGBA8:配置渲染图层的像素格式;
kEAGLRenderingAPIOpenGLES2:指定使用OpenGL的版本是2.0;
2、初始化相关对象
包括帧缓冲、渲染缓冲、着色器程序的初始化,详解看代码中的注释
- (BOOL)initializeBuffers {
BOOL success = YES;
//关闭OpenGL功能:进行深度比较和更新深度缓冲
glDisable( GL_DEPTH_TEST );
////////初始化帧缓冲
//创建帧缓冲,为其申请一个id,赋值给_frameBuffer,1表示申请的个数。
//帧缓冲本质上并不是一个独立的概念,它像是一个管理员,管理着手下的各个缓存,比如颜色缓存、模板缓存、深度缓存等等
glGenFramebuffers( 1, &_frameBuffer );
//绑定帧缓冲,绑定是为了告诉OpenGL在后面引用GL_FRAMEBUFFER引用_frameBuffer
glBindFramebuffer( GL_FRAMEBUFFER, _frameBuffer );
////////初始化渲染缓冲
//创建渲染缓冲,为其申请一个id,赋值给_colorBufferHandle,创建渲染缓存,1表示申请的个数
glGenRenderbuffers( 1, &_colorBuffer );
//绑定渲染缓冲,绑定是告诉OpenGL:我在后面引用GL_RENDERBUFFER的地方,其实是引用_colorRenderBuffer[在引用渲染缓存之前必须绑定当前渲染缓存对象,所以每次使用GL_RENDERBUFFER都要调一次这个]
glBindRenderbuffer( GL_RENDERBUFFER, _colorBuffer );
//把渲染缓冲绑定到渲染图层(CAEAGLLayer)上,并为它分配一个共享内存
[_oglContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer *)self.layer];
//得到当前绑定的渲染缓存对象的一些参数。Target应该是GL_RENDERBUFFER,第二个参数是所要得到的参数名字。最后一个是指向存储返回值的整型量的指针
glGetRenderbufferParameteriv( GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &_width );
glGetRenderbufferParameteriv( GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &_height );
//把 渲染缓存 添加到 帧缓存 的GL_COLOR_ATTACHMENT0附件上,这样整个数据流就串下来了,当数据渲染完之后会放进帧缓冲,实际上数据流向渲染缓冲,渲染缓冲的数据供给渲染图层展示。
glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorBuffer );
if ( glCheckFramebufferStatus( GL_FRAMEBUFFER ) != GL_FRAMEBUFFER_COMPLETE ) {
NSLog( @"Failure with framebuffer generation" );
success = NO;
goto bail;
}
//创建一个纹理缓冲,创建纹理的时候需要用到
CVReturn err = CVOpenGLESTextureCacheCreate( kCFAllocatorDefault, NULL, _oglContext, NULL, &_textureCache );
if ( err ) {
NSLog( @"Error at CVOpenGLESTextureCacheCreate %d", err );
success = NO;
goto bail;
}
//创建着色器程序
_program = glCreateProgram();
//加载顶点着色器代码
GLuint verShader = [self loadShader:GL_VERTEX_SHADER withString:kPassThruVertex];
if (verShader == 0) {
NSLog( @"Error at verShader");
success = NO;
goto bail;
}
//加载片段着色器代码
GLuint fraShader = [self loadShader:GL_FRAGMENT_SHADER withString:kPassThruFragment];
if (fraShader == 0) {
NSLog( @"Error at fraShader");
success = NO;
goto bail;
}
//绑定顶点着色器和片段着色器到着色器程序上
glAttachShader(_program, verShader);
glAttachShader(_program, fraShader);
//一定要在链接程序之前绑定属性,否则拿不到
//第一种方法是通过glBindAttribLocation函数来实现索引和变量之间的对应关系。
//首先,我们为shader中的每个顶点属性变量指定一个索引(一般从0开始)。
//另一种方法则是在shader中直接指定,这是通过GLSL的关键词layout来是实现。为了实现这样的效果,我们需要更改之前的vertex shader的内容。
glBindAttribLocation(_program, ATTRIB_VERTEX, "position");
glBindAttribLocation(_program, ATTRIB_TEXTUREPOSITON, "texturecoordinate");
//链接着色器程序
glLinkProgram(_program);
//链接完需要删除着色器
glDeleteShader(verShader);
glDeleteShader(fraShader);
//获取着色器定义的属性,用于后面对里面的属性进行赋值。
_frame = glGetUniformLocation(_program, "videoframe");
_offset = glGetUniformLocation(_program, "offset");
_uMvpMatrix = glGetUniformLocation(_program, "uMvpMatrix");
bail:
if (!success) {
[self reset];
}
return success;
}
先给个简单着色器代码示例:
//顶点着色器代码
static const char * kPassThruVertex = _STRINGIFY(
attribute vec4 position;
attribute mediump vec4 texturecoordinate;
varying mediump vec2 coordinate;
void main()
{
gl_Position = position;
coordinate = texturecoordinate.xy;
}
);
//片段着色器代码
static const char * kPassThruFragment = _STRINGIFY(
varying highp vec2 coordinate;
uniform sampler2D videoframe;
void main()
{
gl_FragColor = texture2D(videoframe, coordinate);
}
);
着色器是类 C 语言写成,我先对一些关键字做一下简单的解释:
attribute:修饰符只存在于顶点着色器中,用于储存每个顶点信息的输入,比如这里定义了 Position 和 TextureCoords ,用于接收顶点的位置和纹理信息;
vec4 和 vec2:是数据类型,分别指四维向量和二维向量;
mat4:也是数据类型,指4*4矩阵;
varying:修饰符指顶点着色器的输出,同时也是片段着色器的输入,要求顶点着色器和片段着色器中都同时声明,并完全一致,则在片段着色器中可以获取到顶点着色器中的数据;