本文最后更新于:2025年4月9日 下午
前言
这次我们用 OpenGLES 将水印、贴图渲染到相机的预览画面上,代码跟上一篇 iOS 使用 OpenGLES 实现相机画面镜像 差不多。
实现思路
输入的纹理应该有两路,一路是相机回调的 Pixel Buffer,一路是水印图片纹理。相机 Buffer 在视口中的大小始终是不变的,但水印图片 Size 和 Position 都是不固定的,不能使用相同的顶点坐标和视口大小绘制这两个纹理,目前想到的有两种方式可以实现:一种是两个纹理采用两个不同的顶点坐标,同时共享屏幕视口大小;一种是两个纹理采用同一组顶点坐标,采用不同的视口大小。此例采用第二种方式。
代码
顶点坐标还是与上一篇的一致,由于是将渲染结果输出到纹理,因此不需要做翻转,顶点坐标纹理坐标一一对应即可:
| typedef struct { GLKVector2 positionCoordinates; GLKVector2 textureCoordinates; } VerticesCoordinates;
static const 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}} };
|
然后是着色器们,顶点着色器接受顶点坐标和纹理坐标转成 vec4 类型,赋值给 gl_Position,并将纹理坐标给到片段着色器。片段着色器拿到纹理坐标,通过纹理采样器计算像素颜色:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| static const char* vertex_shader_string = "attribute vec4 position;" "attribute vec4 texcoord;" "varying vec2 v_texcoord;" "void main() {" " gl_Position = position;" " v_texcoord = texcoord.xy;" "}";
static const char* fragment_shader_string = "precision mediump float;" "varying vec2 v_texcoord;" "uniform sampler2D tex;" "void main() {" " gl_FragColor = texture2D(tex, v_texcoord);" "}";
|
着色器程序的创建、编译代码跟上一篇一致,就不再贴了,接着看下初始化方法:
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
| - (void)setupGL { _context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3]; if (!_context) { _context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; if (!_context) { NSLog(@"error: video mirror processor, Error! Unable to create an OpenGL ES Context!"); } } if (!_context || ![EAGLContext setCurrentContext:_context]) { NSLog(@"failed to setup EAGLContext"); } CVReturn ret = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, NULL, _context, NULL, &_textureCache); if (ret != kCVReturnSuccess) { NSLog(@"error: video mirror processor, Error! CVOpenGLESTextureCacheCreate failed %d", ret); } glEnable(GL_BLEND); glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); glGenBuffers(1, &_vbo); glBindBuffer(GL_ARRAY_BUFFER, _vbo); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); _program = build_program(vertex_shader_string, fragment_shader_string); glUseProgram(_program); GLint samplerLocation = glGetUniformLocation(_program, "tex"); glUniform1i(samplerLocation, 0); GLint posLocation = glGetAttribLocation(_program, "position"); GLint texLocation = glGetAttribLocation(_program, "texcoord"); glEnableVertexAttribArray(posLocation); glEnableVertexAttribArray(texLocation); glVertexAttribPointer(posLocation, 2, GL_FLOAT, GL_FALSE, sizeof(VerticesCoordinates), (void *)offsetof(VerticesCoordinates, positionCoordinates)); glVertexAttribPointer(texLocation, 2, GL_FLOAT, GL_FALSE, sizeof(VerticesCoordinates), (void *)offsetof(VerticesCoordinates, textureCoordinates)); }
|
两个纹理混合需要将 GL_BLEND 打开,通过 glBlendFunc 设置源因子和目标因子,不同的参数设置会产生不同的纹理叠加效果。FBO 和水印纹理的创建是动态的,所以放到渲染循环里处理:
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
| - (CVPixelBufferRef)process:(CVPixelBufferRef)pixelBuffer { [_renderLock lock]; if (_needStopDisplay || !_watermarkImage) { [_renderLock unlock]; return _outputPixelBuffer; } if ([EAGLContext currentContext] != _context) { [EAGLContext setCurrentContext:_context]; } GLsizei pixelWidth = (GLsizei)CVPixelBufferGetWidth(pixelBuffer); GLsizei pixelHeight = (GLsizei)CVPixelBufferGetHeight(pixelBuffer); if (pixelWidth == 0 || pixelHeight == 0) { [_renderLock unlock]; return _outputPixelBuffer; } if (pixelWidth != _currentRenderSize.width || pixelHeight != _currentRenderSize.height) { _currentRenderSize = CGSizeMake(pixelWidth, pixelHeight); [self setupCVPixelBuffer:&_outputPixelBuffer]; _needUpdateFBO = YES; } if (_needUpdateFBO) { [self setupFBO]; _needUpdateFBO = NO; } if (_needUpdateWatermark) { [self setupWatermarkTexture]; _needUpdateWatermark = NO; } if (_inputTexture) { CFRelease(_inputTexture); _inputTexture = NULL; } CVOpenGLESTextureCacheFlush(_textureCache, 0); CVPixelBufferLockBaseAddress(pixelBuffer, 0); CVReturn ret = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, _textureCache, pixelBuffer, NULL, GL_TEXTURE_2D, GL_RGBA, pixelWidth, pixelHeight, GL_BGRA, GL_UNSIGNED_BYTE, 0, &_inputTexture); if (ret != kCVReturnSuccess) { NSLog(@"error: _inTexture, CVOpenGLESTextureCacheCreateTextureFromImage, failed."); } CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); glBindTexture(CVOpenGLESTextureGetTarget(_inputTexture), CVOpenGLESTextureGetName(_inputTexture)); 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); glUseProgram(_program); glBindFramebuffer(GL_FRAMEBUFFER, _fbo); glBindBuffer(GL_ARRAY_BUFFER, _vbo); glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glViewport(0, 0, _currentRenderSize.width, _currentRenderSize.height); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); glBindTexture(GL_TEXTURE_2D, _watermarkTexture); glViewport(_watermarkPosition.x, _watermarkPosition.y, _watermarkSize.width, _watermarkSize.height); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); glFlush(); glBindFramebuffer(GL_FRAMEBUFFER, 0); [_renderLock unlock]; return _outputPixelBuffer; }
|
process 方法实际是有两次绘制,先是渲染了相机 Buffer,然后调整视口大小为水印的 position 和 size,再渲染水印纹理。
创建输出 Buffer 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| - (void)setupCVPixelBuffer:(CVPixelBufferRef *)pixelBuffer { NSDictionary *pixelBufferOptions = @{ (NSString *)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA), (NSString *)kCVPixelBufferWidthKey : @(_currentRenderSize.width), (NSString *)kCVPixelBufferHeightKey : @(_currentRenderSize.height), (NSString *)kCVPixelBufferOpenGLESCompatibilityKey : @YES, (NSString *)kCVPixelBufferIOSurfacePropertiesKey : @{} }; if (*pixelBuffer) { CVPixelBufferRelease(*pixelBuffer); *pixelBuffer = NULL; } CVReturn ret = CVPixelBufferCreate(kCFAllocatorDefault, _currentRenderSize.width, _currentRenderSize.height, kCVPixelFormatType_32BGRA, (__bridge CFDictionaryRef)pixelBufferOptions, pixelBuffer); if (ret != kCVReturnSuccess) { NSLog(@"error: video mirror processor, Unable to create cvpixelbuffer %d", ret); } }
|
创建 FBO 代码:
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
| - (void)setupFBO { if ([EAGLContext currentContext] != _context) { [EAGLContext setCurrentContext:_context]; } if (_fbo > 0) { glDeleteFramebuffers(1, &_fbo); _fbo = 0; } glGenFramebuffers(1, &_fbo); if (_outputTexture) { CFRelease(_outputTexture); _outputTexture = NULL; } CVReturn ret = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, _textureCache, _outputPixelBuffer, NULL, GL_TEXTURE_2D, GL_RGBA, _currentRenderSize.width, _currentRenderSize.height, GL_BGRA, GL_UNSIGNED_BYTE, 0, &_outputTexture); if (ret != kCVReturnSuccess) { NSLog(@"error: _outputTexture, CVOpenGLESTextureCacheCreateTextureFromImage, failed."); } glBindTexture(CVOpenGLESTextureGetTarget(_outputTexture), CVOpenGLESTextureGetName(_outputTexture)); 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_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glBindFramebuffer(GL_FRAMEBUFFER, _fbo); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, CVOpenGLESTextureGetName(_outputTexture), 0); if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { NSLog(@"error: failed to make complete framebuffer object %x", glCheckFramebufferStatus(GL_FRAMEBUFFER)); } glBindFramebuffer(GL_FRAMEBUFFER, 0); }
|
创建水印图片纹理代码:
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
| - (void)setupWatermarkTexture { if (!CGSizeEqualToSize(_watermarkImage.size, _watermarkSize)) { UIGraphicsBeginImageContext(_watermarkSize); [_watermarkImage drawInRect:CGRectMake(0, 0, _watermarkSize.width, _watermarkSize.height)]; UIImage *scaledImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); _watermarkImage = scaledImage; } if (_watermarkTexture) { glDeleteTextures(1, &_watermarkTexture); _watermarkTexture = 0; } NSError *error; GLKTextureInfo *textureInfo = [GLKTextureLoader textureWithCGImage:_watermarkImage.CGImage options:nil error:&error]; if (error) { NSLog(@"error: create watermark texture failed: %@", error.localizedDescription); } glBindTexture(textureInfo.target, textureInfo.name); 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); _watermarkTexture = textureInfo.name; }
|
这里使用 GLKTextureLoader 生成纹理对象时发现一个问题,_watermarkImage.CGImage 明明有值但 textureWithCGImage 方法一直在报错,报错信息是 The operation couldn’t be completed. (GLKTextureLoaderErrorDomain error 8.) 。通过在每行 OpenGL 代码后增加 NSLog(@"GL Error = %u", glGetError()); 打印错误信息,发现是在给片段着色器的纹理采样器绑定纹理单元时方法用错了 glUniform1i 写成了 glUniform1f,修改之后运行没有问题,也算了解了一种 OpenGL 报错的排查方式。
后记
代码完成后就可以放到相机的回调方法里使用了。最近一直在看 OpenGLES 的代码,目前工程里还有一些相关的工具需要写,比如像素格式转换,Buffer 裁剪的功能,之后也会总结到博客上。哇今天还是比较高产,写了两篇博客,晚上可以加个鸡腿了,疫情期间鸡腿可是奢侈品呐。。。