本文最后更新于:2025年4月9日 下午
前言
上一篇有提到 使用 AVFoundation 采集相机画面,并渲染到苹果内置的 AVCaptureVideoPreviewLayer 图层上,代码很简单,但使用上有很大局限性。AVCaptureSession 采集到的原始视频帧直接给到了 AVCaptureVideoPreviewLayer 用于渲染,我们没办法在中间环节处理视频帧数据,美颜滤镜也就没办法实现,所以我们需要借助 OpenGLES 自己实现画面渲染。
苹果的 GLKit 框架对 OpenGLES 的部分接口调用做了封装,使用起来非常方便。我们的需求可以直接使用 GLKView 实现,开发的代码量是比较少的,但为了熟悉 OpenGLES 的接口调用和管线渲染流程,我们还是使用最原始的方法,基于 CAEAGLLayer,自己来实现着色器代码。
代码实现
准备工作
使用 CAEAGLLayer
首先在项目中创建一个继承 UIView 的 XPGLKView,如果需要在图层上自定义 OpenGL 渲染,需要将 UIView 的 layerClass 设置为 CAEAGLLayer:
| @implementation XPGLKView
+(Class)layerClass { return [CAEAGLLayer class]; }
|
顶点数据计算
我们需要计算两类坐标,一类是 OpenGL 顶点坐标,一类是纹理贴图的坐标,先定义一个结构体类型,包含两类坐标:
| typedef struct { GLKVector2 positionCoordinates; GLKVector2 textureCoordinates; } VerticesCoordinates;
|
我们的场景只需要 4 个顶点,使用 glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); 即可绘制两个三角形,按照顶点坐标和纹理坐标一一对应的关系,可以得到一个初步的顶点数据:
| VerticesCoordinates vertices[] = { {{-1.0f, -1.0f}, {0.0f, 0.0f}}, {{ 1.0f, -1.0f}, {1.0f, 0.0f}}, {{-1.0f, 1.0f}, {0.0f, 1.0f}}, {{ 1.0f, 1.0f}, {1.0f, 1.0f}} };
|
上面看着没什么问题,但实际渲染发现图像上下颠倒了,原因是使用 CoreVideo 框架读取 Pixel Buffer 数据时是按照屏幕坐标原点(左上角)开始的,而纹理坐标正好相反,所以我们需要翻转一下纹理坐标的 Y 轴,得到最终的顶点数据:
| VerticesCoordinates vertices[] = { {{-1.0f, -1.0f}, {0.0f, 1.0f}}, {{ 1.0f, -1.0f}, {1.0f, 1.0f}}, {{-1.0f, 1.0f}, {0.0f, 0.0f}}, {{ 1.0f, 1.0f}, {1.0f, 0.0f}} };
|
着色器程序
为了代码整洁,我们封装一个着色器程序,包含着色器的初始化、编译和链接功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
| - (instancetype)initWithVertexShaderString:(NSString *)vShaderString fragmentShaderString:(NSString *)fShaderString { self = [super init]; if (self) { _attributes = [NSMutableArray array]; _ID = glCreateProgram(); if (![self compileShader:&_vertexShader type:GL_VERTEX_SHADER string:vShaderString]) { NSLog(@"Failed to compile vertex shader"); } if (![self compileShader:&_fragmentShader type:GL_FRAGMENT_SHADER string:fShaderString]) { NSLog(@"Failed to compile fragment shader"); } glAttachShader(_ID, _vertexShader); glAttachShader(_ID, _fragmentShader); } return self; }
- (void)dealloc { if (_vertexShader) { glDeleteShader(_vertexShader); } if (_fragmentShader) { glDeleteShader(_fragmentShader); } if (_ID) { glDeleteProgram(_ID); } }
- (BOOL)compileShader:(GLuint *)shader type:(GLenum)type string:(NSString *)shaderString { GLint status; const GLchar *source = (GLchar *)[shaderString UTF8String]; if (!source) { NSLog(@"Failed to load shader string"); return NO; } *shader = glCreateShader(type); glShaderSource(*shader, 1, &source, NULL); glCompileShader(*shader); glGetShaderiv(*shader, GL_COMPILE_STATUS, &status); if (status != GL_TRUE) { GLint logLength; glGetShaderiv(*shader, GL_INFO_LOG_LENGTH, &logLength); if (logLength) { GLchar *log = (GLchar *)malloc(logLength); glGetShaderInfoLog(*shader, logLength, &logLength, log); if (shader == &_vertexShader) { self.vertexShaderLog = [NSString stringWithFormat:@"%s", log]; } else { self.fragmentShaderLog = [NSString stringWithFormat:@"%s", log]; } free(log); } } return status == GL_TRUE; }
- (BOOL)link { GLint status; glLinkProgram(_ID); glGetProgramiv(_ID, GL_LINK_STATUS, &status); if (status == GL_FALSE) { return NO; } if (_vertexShader) { glDeleteShader(_vertexShader); _vertexShader = 0; } if (_fragmentShader) { glDeleteShader(_fragmentShader); _fragmentShader = 0; } return YES; }
- (void)use { glUseProgram(_ID); }
- (void)addAttribute:(NSString *)attributeName { if (![_attributes containsObject:attributeName]) { [_attributes addObject:attributeName]; glBindAttribLocation(_ID, (GLuint)[_attributes indexOfObject:attributeName], [attributeName UTF8String]); } }
- (GLuint)getAttributeLocation:(NSString *)attributeName { return (GLuint)[_attributes indexOfObject:attributeName]; }
- (GLuint)getUniformLocation:(NSString *)uniformName { return glGetUniformLocation(_ID, [uniformName UTF8String]); }
- (void)showError { NSString *progLog = [self programLog]; NSLog(@"Program link log: %@", progLog); NSString *fragLog = [self fragmentShaderLog]; NSLog(@"Fragment shader compile log: %@", fragLog); NSString *vertLog = [self vertexShaderLog]; NSLog(@"Vertex shader compile log: %@", vertLog); }
|
有了着色器程序,再来看下顶点着色器和片段着色器的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| static NSString *XP_GLK_VSH = @" \ attribute vec4 position; \ attribute vec4 inputTextureCoordinate; \ varying vec2 textureCoordinate; \ \ void main() \ { \ gl_Position = position; \ textureCoordinate = inputTextureCoordinate.xy; \ } \ ";
static NSString *XP_GLK_FSH = @" \ varying highp vec2 textureCoordinate; \ uniform sampler2D inputImageTexture; \ \ void main() \ { \ gl_FragColor = texture2D(inputImageTexture, textureCoordinate); \ } \ ";
|
有了顶点数据和着色器程序,接下来看一下 XPGLKView 的初始化代码。
初始化
初始化方法中设置一些变量的默认值,主要还是配置 OpenGL 环境:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
| - (void)commonInit { _needStopDisplay = NO; _fillMode = XPVideoFillModeAspectFill; _renderLock = [[NSRecursiveLock alloc] init]; [self addObservers]; CAEAGLLayer *layer = (CAEAGLLayer *)self.layer; layer.opaque = YES; layer.contentsScale = [[UIScreen mainScreen] scale]; layer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:NO], kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat, nil]; if (!_context) { _context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3]; if (!_context) { _context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; } } if (!_context || ![EAGLContext setCurrentContext:_context]) { NSLog(@"failed to setup EAGLContext"); } _backgroundColor = GLKVector4Make(0.0f, 0.0f, 0.0f, 1.0f); CVReturn ret = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, NULL, _context, NULL, &_textureCache); if (ret != kCVReturnSuccess) { NSLog(@"CVOpenGLESTextureCacheCreate: %d", ret); } _program = [[XPGLKProgram alloc] initWithVertexShaderString:XP_GLK_VSH fragmentShaderString:XP_GLK_FSH]; [_program addAttribute:@"position"]; [_program addAttribute:@"inputTextureCoordinate"]; if (![_program link]) { [_program showError]; NSLog(@"Filter shader link failed"); _program = nil; } glGenFramebuffers(1, &_frameBuffer); glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer); glGenRenderbuffers(1, &_renderBuffer); glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer); [_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer *)self.layer]; GLint backingWidth, backingHeight; glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &backingWidth); glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &backingHeight); if (backingWidth == 0 || backingHeight == 0) { [self destroyFrameBuffer]; return; } _viewportSize.width = (CGFloat)backingWidth; _viewportSize.height = (CGFloat)backingHeight; glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _renderBuffer); GLuint framebufferStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER); if (framebufferStatus != GL_FRAMEBUFFER_COMPLETE) { NSLog(@"Fail setup GL framebuffer %d:%d", backingWidth, backingHeight); } else { NSLog(@"Success setup GL framebuffer %d:%d", backingWidth, backingHeight); } glGenBuffers(1, &_vertexBuffer); glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer); glBufferData(GL_ARRAY_BUFFER, sizeof(_vertices), _vertices, GL_DYNAMIC_DRAW); _positionAttribute = [_program getAttributeLocation:@"position"]; _textureCoordinateAttribute = [_program getAttributeLocation:@"inputTextureCoordinate"]; _inputTextureUniform = [_program getUniformLocation:@"inputImageTexture"]; glEnableVertexAttribArray(_positionAttribute); glEnableVertexAttribArray(_textureCoordinateAttribute); }
|
首先通过 layer 配置帧缓冲区的一些属性,随后初始化并绑定上下文,创建我们封装过的着色器程序,创建帧缓存、顶点缓存,最后将顶点着色器属性打开。
渲染
初始化之后,就可以准备渲染画面了,渲染方法触发的时机是 AVCaptureSession 的相机数据帧回调,我们在回调里切换渲染线程,调用 displayPixelBuffer 方法:
| #pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate - (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection { CVPixelBufferRef originPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); XPDispatchSync(self.renderQueue, cameraSourceRenderQueueKey, ^{ [self.previewView displayPixelBuffer:originPixelBuffer]; }); }
|
渲染的方法实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
| - (void)displayPixelBuffer:(CVPixelBufferRef)pixelBuffer { [_renderLock lock]; if (_needStopDisplay) { [_renderLock unlock]; return; } if ([EAGLContext currentContext] != _context) { [EAGLContext setCurrentContext:_context]; } [_program use]; glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer); glViewport(0, 0, (GLint)_viewportSize.width, (GLint)_viewportSize.height); glClearColor(_backgroundColor.r, _backgroundColor.g, _backgroundColor.b, _backgroundColor.a); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); if (_texture) { CFRelease(_texture); _texture = NULL; } CVOpenGLESTextureCacheFlush(_textureCache, 0); size_t frameWidth = CVPixelBufferGetWidth(pixelBuffer); size_t frameHeight = CVPixelBufferGetHeight(pixelBuffer); CVPixelBufferLockBaseAddress(pixelBuffer, 0); CVReturn ret = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, _textureCache, pixelBuffer, NULL, GL_TEXTURE_2D, GL_RGBA, (GLsizei)frameWidth, (GLsizei)frameHeight, GL_BGRA, GL_UNSIGNED_BYTE, 0, &_texture); if (!_texture || ret != kCVReturnSuccess) { NSLog(@"error: Mapping texture:%d", ret); } CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); glActiveTexture(GL_TEXTURE0); glBindTexture(CVOpenGLESTextureGetTarget(_texture), CVOpenGLESTextureGetName(_texture)); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_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); glUniform1i(_inputTextureUniform, 0); [self updateInputImageSize:CGSizeMake(frameWidth, frameHeight)]; glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer); glVertexAttribPointer(_positionAttribute, 2, GL_FLOAT, GL_FALSE, sizeof(VerticesCoordinates), (void *)offsetof(VerticesCoordinates, positionCoordinates)); glVertexAttribPointer(_textureCoordinateAttribute, 2, GL_FLOAT, GL_FALSE, sizeof(VerticesCoordinates), (void *)offsetof(VerticesCoordinates, textureCoordinates)); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer); [_context presentRenderbuffer:GL_RENDERBUFFER]; [_renderLock unlock]; }
|
适配填充模式
按目前的顶点和纹理坐标,渲染内容默认会铺满整个图层,如果相机回调的 Pixel Buffer 宽高与图层宽高不一致,会产生拉伸的现象,因此需要适配不同的填充模式,我们定义了三种填充模式:
| typedef NS_ENUM(NSUInteger, XPVideoFillMode) { XPVideoFillModeStretch, XPVideoFillModeAspectFit, XPVideoFillModeAspectFill, };
|
每种填充模式的顶点坐标计算方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| - (void)recalculateVerticesCoordinates { CGSize currentViewSize = _currentBoundsSize; CGRect currentViewBounds = CGRectMake(0, 0, currentViewSize.width, currentViewSize.height); CGRect insetRect = AVMakeRectWithAspectRatioInsideRect(_inputImageSize, currentViewBounds); CGFloat heightScaling, widthScaling; switch (_fillMode) { case XPVideoFillModeStretch: widthScaling = 1.0f; heightScaling = 1.0f; break; case XPVideoFillModeAspectFit: widthScaling = insetRect.size.width / currentViewSize.width; heightScaling = insetRect.size.height / currentViewSize.height; break; case XPVideoFillModeAspectFill: { widthScaling = currentViewSize.height / insetRect.size.height; heightScaling = currentViewSize.width / insetRect.size.width; } break; default: break; } _vertices[0].positionCoordinates = GLKVector2Make(-widthScaling, -heightScaling); _vertices[1].positionCoordinates = GLKVector2Make(widthScaling, -heightScaling); _vertices[2].positionCoordinates = GLKVector2Make(-widthScaling, heightScaling); _vertices[3].positionCoordinates = GLKVector2Make(widthScaling, heightScaling);
_vertices[0].textureCoordinates = GLKVector2Make(0.0f, 1.0f); _vertices[1].textureCoordinates = GLKVector2Make(1.0f, 1.0f); _vertices[2].textureCoordinates = GLKVector2Make(0.0f, 0.0f); _vertices[3].textureCoordinates = GLKVector2Make(1.0f, 0.0f); }
|
在渲染过程中,将顶点数据更新同步到 GPU:
| glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(_vertices), _vertices);
|
后记
相机画面的自定义渲染没有用到 OpenGL 的高级功能,主要是对 OpenGL 对象、纹理、管线概念的理解,和 Core Animation、Core Video 的交互。下面贴一些文档可以用来参考:
1.OpenGL 中文手册 https://learnopengl-cn.github.io/
2.iOS 核心动画高级技巧 https://zsisme.gitbooks.io/ios-/content/index.html
3.OpenGLES 苹果官网文档 https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/OpenGLES_ProgrammingGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008793-CH1-SW1
4.《OpenGL ES应用开发实践 指南 iOS卷》