zoukankan      html  css  js  c++  java
  • 【AR实验室】OpenGL ES绘制相机(OpenGL ES 1.0版本)

    0x00 - 前言


    之前做一些移动端的AR应用以及目前看到的一些AR应用,基本上都是这样一个套路:手机背景显示现实场景,然后在该背景上进行图形学绘制。至于图形学绘制时,相机外参的解算使用的是V-SLAM、Marker-Based还是GPS的方法,就不一而足了。

    绘制相机背景1

    所以说要在手机上进行现实场景的展现也是目前AR应用一个比较重要的模块。一般来说,在移动端,基本上都是使用OpenGL ES进行绘制。所以我们优先考虑使用OpenGL ES进行相机的绘制。当然,有些应用直接利用iOS的UIImage进行相机场景的展示,这也是可以的,不过考虑到与OpenGL ES的绘制环境兼容性、Android端的复用情况以及UIImage的效率情况,我决定还是使用OpenGL ES进行绘制,这样与后面的图形绘制(OpenGL ES)可以统一绘制环境,另外OpenGL ES是可以跨平台的,代码也可以很方便地移植到Android端,并且OpenGL ES比UIImage更接近图形硬件,所以效率上要快那么一丢丢。

    利用相机绘制部分其实已经有一些解决方案了,但是基本上每个应用的绘制方式都不一样。目前来说我看到过比较好的就是ARToolKit的方式,但是ARToolKit工程化程度已经很高了,想将其中的相机绘制部分分离出来为自己所用,对于渣渣的我来说,两个字——“太难”。所以此处我自己写了一个相机绘制的模块,虽然说在鲁棒性上还差很多,但是基本可以用来做做小Demo。如果大家想做一个商用的AR应用,建议直接使用ARToolKit的相机绘制代码。

    0x01 - 思路


    因为我只会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的绘制。

    绘制思路1

    0x02 - AVCaptureSession获取拍摄内容


    AVCaptureSession使用流程主要分为两部分。第一部分是配置相机输入输出的功能参数,比如拍摄分辨率、相机焦距、曝光、白平衡等等。另一部分是利用AVCaptureVideoDataOutputSampleBufferDelegate这个代理中的函数

    - (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection;

    获取到具体的拍摄内容。

    2.1 配置相机功能参数

    配置相机功能参数其实就是配置AVCaptureSession对象。这里面主要涉及到四个类AVCaptureSession、AVCaptureDevice、AVCaptureDeviceInput和AVCaptureVideoDataOutput。这四个类的关系如下:

    AVCaptureSession是管理AVCaptureDeviceInput和AVCaptureVideoDataOutput,也就是管理输入输出过程,所以称作Session。相机的输入配置就是AVCaptureDeviceInput,主要解决是否使用自动曝光、自动白平衡之类的,而输出配置就是AVCaptureVideoDataOutput,主要决定输出视频图像的格式之类的。AVCaptureDevice表示捕捉设备,因为具体捕获的内容不明确,所以还会区分捕捉视频的设备还是捕捉声音的设备。这里我们从捕捉这个词可以看出其实AVCaptureDevice和输入AVCaptureDeviceInput关系紧密。

    AVCaptureSession组织结构

    简单介绍一下代码中对于AVCaptureSession对象session的配置:

    - (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);
        
        // 绘制部分
        // ...
    }

    2.2 获取拍摄内容

    设置好了相机的各种参数,同时启动Session,就可以在函数

    - (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);
    }

    0x03 – OpenGL ES绘制相机


    有了相机捕获的每帧图像后,就可以使用贴纹理的方式将其绘制在手机屏幕上了。但是在这之前还需要做一件事情,那就是初始化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

    0x04-效果显示


    因为我使用的为iPhone5s,分辨率为320x568,而相机图像分辨率为480x640。所以为了让图像全部能显示在屏幕上,我选择了等宽显示。

    绘制相机效果1

    为了方便大家使用代码,现已将代码提交到GitHub上了,请猛戳此处

    0x05-参考资料


  • 相关阅读:
    出现socket:(10107)系统调用失败
    JS面向对象基础讲解(工厂模式、构造函数模式、原型模式、混合模式、动态原型模式)
    获取滚动条距离底部的距离
    linux常用命令使用方法
    Python:一
    【C++ Primer 第15章】定义派生类拷贝构造函数、赋值运算符
    【【C++ Primer 第15章】 虚析构函数
    ubuntu基本用法
    深度优先搜索(DFS)和广度优先搜索(BFS)
    【C++ Primer 第7章】定义抽象数据类型
  • 原文地址:https://www.cnblogs.com/polobymulberry/p/6130376.html
Copyright © 2011-2022 走看看