以前作一些移动端的AR应用以及目前看到的一些AR应用,基本上都是这样一个套路:手机背景显示现实场景,而后在该背景上进行图形学绘制。至于图形学绘制时,相机外参的解算使用的是V-SLAM、Marker-Based仍是GPS的方法,就不一而足了。git
因此说要在手机上进行现实场景的展示也是目前AR应用一个比较重要的模块。通常来讲,在移动端,基本上都是使用OpenGL ES进行绘制。因此咱们优先考虑使用OpenGL ES进行相机的绘制。固然,有些应用直接利用iOS的UIImage进行相机场景的展现,这也是能够的,不过考虑到与OpenGL ES的绘制环境兼容性、Android端的复用状况以及UIImage的效率状况,我决定仍是使用OpenGL ES进行绘制,这样与后面的图形绘制(OpenGL ES)能够统一绘制环境,另外OpenGL ES是能够跨平台的,代码也能够很方便地移植到Android端,而且OpenGL ES比UIImage更接近图形硬件,因此效率上要快那么一丢丢。github
利用相机绘制部分其实已经有一些解决方案了,可是基本上每一个应用的绘制方式都不同。目前来讲我看到过比较好的就是ARToolKit的方式,可是ARToolKit工程化程度已经很高了,想将其中的相机绘制部分分离出来为本身所用,对于渣渣的我来讲,两个字——“太难”。因此此处我本身写了一个相机绘制的模块,虽说在鲁棒性上还差不少,可是基本能够用来作作小Demo。若是你们想作一个商用的AR应用,建议直接使用ARToolKit的相机绘制代码。数组
由于我只会iOS,因此这里主要讲解的是在iOS上利用OpenGL ES绘制相机。另外,相对于OpenGL ES 2.0,1.0更为简单,因此此处使用的OpenGL ES版本为1.0,固然,后面确定会兼容2.0。缓存
咱们都知道iOS中相机的绘制离不开AVCaptureSession。利用AVCaptureSession能够获取到实时相机拍摄内容。随后利用OpenGL ES中绘制纹理的方式将该内容绘制到屏幕上。整个思路就是这么简单。主要涉及两个部分,一个是AVCaptureSession的使用,一个是iOS上OpenGl ES的绘制。session
AVCaptureSession使用流程主要分为两部分。第一部分是配置相机输入输出的功能参数,好比拍摄分辨率、相机焦距、曝光、白平衡等等。另外一部分是利用AVCaptureVideoDataOutputSampleBufferDelegate这个代理中的函数ide
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection;
获取到具体的拍摄内容。函数
配置相机功能参数其实就是配置AVCaptureSession对象。这里面主要涉及到四个类AVCaptureSession、AVCaptureDevice、AVCaptureDeviceInput和AVCaptureVideoDataOutput。这四个类的关系以下:ui
AVCaptureSession是管理AVCaptureDeviceInput和AVCaptureVideoDataOutput,也就是管理输入输出过程,因此称做Session。相机的输入配置就是AVCaptureDeviceInput,主要解决是否使用自动曝光、自动白平衡之类的,而输出配置就是AVCaptureVideoDataOutput,主要决定输出视频图像的格式之类的。AVCaptureDevice表示捕捉设备,由于具体捕获的内容不明确,因此还会区分捕捉视频的设备仍是捕捉声音的设备。这里咱们从捕捉这个词能够看出其实AVCaptureDevice和输入AVCaptureDeviceInput关系紧密。atom
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection { // 时间戳,之后的文章须要该信息。此处能够忽略 CMTime timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer); if (CMTIME_IS_VALID(self.preTimeStamp)) { self.videoFrameRate = 1.0 / CMTimeGetSeconds(CMTimeSubtract(timestamp, self.preTimeStamp)); } self.preTimeStamp = timestamp; // 获取图像缓存区内容 CVImageBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); // 锁定pixelBuffer的基址,与下面解锁基址成对 // CVPixelBufferLockBaseAddress要传两个参数 // 第一个参数是你要锁定的buffer的基址,第二个参数目前还未定义,直接传'0'便可 CVPixelBufferLockBaseAddress(pixelBuffer, 0); // 获取图像缓存区的宽高 int buffWidth = static_cast<int>(CVPixelBufferGetWidth(pixelBuffer)); int buffHeight = static_cast<int>(CVPixelBufferGetHeight(pixelBuffer)); // 这一步很重要,将图像缓存区的内容转化为C语言中的unsigned char指针 // 由于咱们在相机设置时,图像格式为BGRA,然后面OpenGL ES的纹理格式为RGBA // 这里使用OpenCV转换格式,固然,你也能够不用OpenCV,手动直接交换R和B两个份量便可 unsigned char* imageData = (unsigned char*)CVPixelBufferGetBaseAddress(pixelBuffer); _imgMat = cv::Mat(buffWidth, buffHeight, CV_8UC4, imageData); cv::cvtColor(_imgMat, _imgMat, CV_BGRA2RGBA); // 解锁pixelBuffer的基址 CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); // 绘制部分 // ... }
设置好了相机的各类参数,同时启动Session,就能够在函数spa
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
中获取到每帧图像,并进行处理。
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection { // 时间戳,之后的文章须要该信息。此处能够忽略 CMTime timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer); if (CMTIME_IS_VALID(self.preTimeStamp)) { self.videoFrameRate = 1.0 / CMTimeGetSeconds(CMTimeSubtract(timestamp, self.preTimeStamp)); } self.preTimeStamp = timestamp; // 获取图像缓存区内容 CVImageBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); // 锁定pixelBuffer的基址 // CVPixelBufferLockBaseAddress要传两个参数 // 第一个参数是你要锁定的buffer的基址,第二个参数目前还未定义,直接传'0'便可 CVPixelBufferLockBaseAddress(pixelBuffer, 0); // 获取图像缓存区的宽高 int buffWidth = static_cast<int>(CVPixelBufferGetWidth(pixelBuffer)); int buffHeight = static_cast<int>(CVPixelBufferGetHeight(pixelBuffer)); // 这一步很重要,将图像缓存区的内容转化为C语言中的unsigned char指针 // 由于咱们在相机设置时,图像格式为BGRA,然后面OpenGL ES的纹理格式为RGBA // 这里使用OpenCV转换格式,固然,你也能够不用OpenCV,手动直接交换R和B两个份量便可 unsigned char* imageData = (unsigned char*)CVPixelBufferGetBaseAddress(pixelBuffer); cv::Mat imgMat(buffWidth, buffHeight, CV_8UC4, imageData); cv::cvtColor(imgMat, imgMat, CV_BGRA2RGBA); }
有了相机捕获的每帧图像后,就可使用贴纹理的方式将其绘制在手机屏幕上了。可是在这以前还须要作一件事情,那就是初始化iOS的OpenGL ES 1.0绘制环境。
这里咱们将一个普通UIView设置为能够进行OpenGL ES 1.0进行绘制的EAGLView。
@implementation EAGLView // 默认UIView的layerClass为[CALayer class] // 重写layerClass为CAEAGLLayer,这样self.layer返回的就不是CALayer // 而是支持OpenGL ES的CAEAGLLayer + (Class)layerClass { return [CAEAGLLayer class]; } #pragma mark - init methods - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { CAEAGLLayer *eaglLayer = (CAEAGLLayer *)self.layer; // layer默认时透明的,只有设置为不透明才能看见 eaglLayer.opaque = TRUE; // 配置eaglLayer的绘制属性 // kEAGLDrawablePropertyRetainedBacking不维持上一次绘制内容,也就说每次绘制以前都重置一下以前的绘制内容 // kEAGLDrawablePropertyColorFormat像素格式为RGBA,注意和相机直接给的BGRA不一致,须要转换 eaglLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithBool:FALSE], kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat, nil]; // 此处使用OpenGL ES 1.0进行绘制,因此实例化ES1Renderer // ES1Renderer表示的是OpenGL ES 1.0绘制环境,后面详解 if (!_renderder) { _renderder = [[ES1Renderer alloc] init]; if (!_renderder) { return nil; } } } return self; } #pragma mark - life cycles - (void)layoutSubviews { // 利用renderer渲染器进行绘制 [_renderder resizeFromLayer:(CAEAGLLayer *)self.layer]; } @end
上述咱们提供了EAGLView,至关于给OpenGL ES提供了画布。而代码中的renderer是一个具备渲染功能的对象,相似于画笔。考虑到之后须要兼容OpenGL ES 1.0和2.0,因此抽象了一个ESRenderProtocol协议,OpenGL ES 1.0和2.0分别实现该协议中方法,这样EAGLView就不须要关心在不一样的OpenGL ES环境中不一样的绘制实现。这里主要使用OpenGL ES 1.0,对应的就是ES1Renderer类,注意ES1Renderer须要遵循ESRenderProtocol协议。下面为ES1Renderer.h内容。
#import <Foundation/Foundation.h> #import <OpenGLES/ES1/gl.h> #import <OpenGLES/ES1/glext.h> #import "ESRenderProtocol.h" @class PJXVideoBuffer; @interface ES1Renderer : NSObject <ESRenderProtocol> // OpenGL ES绘制上下文环境 // 只有在在当前线程中设置好了该上下文环境,才能使用OpenGL ES的功能 @property (nonatomic, strong) EAGLContext *context; // 绘制camera的纹理id @property (nonatomic, assign) GLuint camTexId; // render buffer和frame buffer @property (nonatomic, assign) GLuint defaultFrameBuffer; @property (nonatomic, assign) GLuint colorRenderBuffer; // 获取到render buffer的宽高 @property (nonatomic, assign) GLint backingWidth; @property (nonatomic, assign) GLint backingHeight; // 引用了videoBuffer,主要用于启动捕捉图像的Session以及获取捕捉到的图像 @property (nonatomic, strong) PJXVideoBuffer *videoBuffer; @end
ES1Renderer.mm内容,主要是构建绘制上下文环境,并将videoBuffer生成的相机图像变成纹理绘制到屏幕上。
#import "ES1Renderer.h" #import "PJXVideoBuffer.h" @implementation ES1Renderer #pragma mark - init methods // 1.构建和设置绘制上下文环境 // 2.生成frame buffer和render buffer并绑定 // 3.生成相机纹理 - (instancetype)init { if (self = [super init]) { // 构建OpenGL ES 1.0绘制上下文环境 _context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1]; // 设置当前绘制上下文环境为OpenGL ES 1.0 if (!_context || ![EAGLContext setCurrentContext:_context]) { return nil; } // 生成frame buffer和render buffer // frame buffer并非一个真正的buffer,而是用来管理render buffer、depth buffer、stencil buffer // render buffer至关于主要是存储像素值的 // 因此须要glFramebufferRenderbufferOES将render buffer绑定到frame buffer的GL_COLOR_ATTACHMENT0_OES上 glGenFramebuffersOES(1, &_defaultFrameBuffer); glGenRenderbuffersOES(1, &_colorRenderBuffer); glBindFramebufferOES(GL_FRAMEBUFFER_OES, _defaultFrameBuffer); glBindRenderbufferOES(GL_RENDERBUFFER_OES, _colorRenderBuffer); glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES, GL_COLOR_ATTACHMENT0_OES, GL_RENDERBUFFER_OES, _colorRenderBuffer); // 构建一个绘制相机的纹理 _camTexId = [self genTexWithWidth:640 height:480]; } return self; } #pragma mark - private methods // 构建一个宽width高height的纹理对象 - (GLuint)genTexWithWidth:(GLuint)width height:(GLuint)height { GLuint texId; // 生成并绑定纹理对象 glGenTextures(1, &texId); glBindTexture(GL_TEXTURE_2D, texId); // 注意这里纹理的像素格式为GL_RGBA glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); // 各类纹理参数,这里不赘述 glTexParameterf(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_FALSE); 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); // 解绑纹理对象 glBindTexture(GL_TEXTURE_2D, 0); return texId; } #pragma mark - ESRenderProtocol - (void)render { // 设置绘制上下文 [EAGLContext setCurrentContext:_context]; glBindFramebufferOES(GL_FRAMEBUFFER_OES, _defaultFrameBuffer); // 相机纹理坐标 static GLfloat spriteTexcoords[] = { 0,0, 1,0, 0,1, 1,1}; // 相机顶点坐标 static GLfloat spriteVertices[] = { 0,0, 0,640, 480,0, 480,640}; // 清除颜色缓存 glClearColor(0.0, 0.0, 0.0, 1.0); glClear(GL_COLOR_BUFFER_BIT); // 视口矩阵 glViewport(0, 0, _backingWidth, _backingHeight); // 投影矩阵 glMatrixMode(GL_PROJECTION); glLoadIdentity(); // 正投影 glOrthof(480, 0, _backingHeight*480/_backingWidth, 0, 0, 1); // 852 = 568*480/320 // 模型视图矩阵 glMatrixMode(GL_MODELVIEW); glLoadIdentity(); // OpenGL ES使用的是状态机方式 // 如下开启的意义是在GPU上分配对应空间 glEnableClientState(GL_VERTEX_ARRAY); // 开启顶点数组 glEnableClientState(GL_TEXTURE_COORD_ARRAY); // 开启纹理坐标数组 glEnable(GL_TEXTURE_2D); // 开启2D纹理 // 由于spriteVertices、spriteTexcoords、_camTexId还在CPU内存,须要传递给GPU处理 // 将spriteVertices传递到顶点数组中 glVertexPointer(2, GL_FLOAT, 0, spriteVertices); // 将spriteTexcoords传递到纹理坐标数组中 glTexCoordPointer(2, GL_FLOAT, 0, spriteTexcoords); // 将camTexId纹理对象绑定到2D纹理 glBindTexture(GL_TEXTURE_2D, _camTexId); // 根据videoBuffer获取imgMat(相机图像) glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 640, 480, GL_RGBA, GL_UNSIGNED_BYTE, _videoBuffer.imgMat.data); // 绘制纹理 glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); // 解绑2D纹理 glBindTexture(GL_TEXTURE_2D, 0); // 与上面的glEnable*一一对应 glDisable(GL_TEXTURE_2D); glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_TEXTURE_COORD_ARRAY); // 将render buffer内容绘制到屏幕上 glBindRenderbufferOES(GL_RENDERBUFFER_OES, _colorRenderBuffer); [_context presentRenderbuffer:GL_RENDERBUFFER_OES]; } - (BOOL)resizeFromLayer:(CAEAGLLayer *)layer { // 与init中相似,从新绑定一下而已 glBindRenderbufferOES(GL_RENDERBUFFER_OES, _colorRenderBuffer); [_context renderbufferStorage:GL_RENDERBUFFER_OES fromDrawable:layer]; glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES, GL_RENDERBUFFER_WIDTH_OES, &_backingWidth); glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES, GL_RENDERBUFFER_HEIGHT_OES, &_backingHeight); // 状态检查 if (glCheckFramebufferStatusOES(GL_FRAMEBUFFER_OES) != GL_FRAMEBUFFER_COMPLETE_OES) { PJXLog(@"Failed to make complete framebuffer object %x", glCheckFramebufferStatusOES(GL_FRAMEBUFFER_OES)); return NO; } // 实例化videoBuffer并启动捕获图像任务 if (_videoBuffer == nil) { // 注意PJXVideoBuffer的delegate为ES1Renderer,主要在videoBuffer中执行render函数来绘制相机 _videoBuffer = [[PJXVideoBuffer alloc] initWithDelegate:self]; [_videoBuffer.session startRunning]; } return YES; } @end
由于我使用的为iPhone5s,分辨率为320x568,而相机图像分辨率为480x640。因此为了让图像所有能显示在屏幕上,我选择了等宽显示。
为了方便你们使用代码,现已将代码提交到GitHub上了,请猛戳此处。