【AR實驗室】OpenGL ES繪製相機(OpenGL ES 1.0版本)

0x00 - 前言


以前作一些移動端的AR應用以及目前看到的一些AR應用,基本上都是這樣一個套路:手機背景顯示現實場景,而後在該背景上進行圖形學繪製。至於圖形學繪製時,相機外參的解算使用的是V-SLAM、Marker-Based仍是GPS的方法,就不一而足了。git

繪製相機背景1

因此說要在手機上進行現實場景的展示也是目前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的相機繪製代碼。數組

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的繪製。session

繪製思路1

0x02 - AVCaptureSession獲取拍攝內容


AVCaptureSession使用流程主要分爲兩部分。第一部分是配置相機輸入輸出的功能參數,好比拍攝分辨率、相機焦距、曝光、白平衡等等。另外一部分是利用AVCaptureVideoDataOutputSampleBufferDelegate這個代理中的函數ide

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

獲取到具體的拍攝內容。函數

2.1 配置相機功能參數

配置相機功能參數其實就是配置AVCaptureSession對象。這裏面主要涉及到四個類AVCaptureSession、AVCaptureDevice、AVCaptureDeviceInput和AVCaptureVideoDataOutput。這四個類的關係以下:ui

AVCaptureSession是管理AVCaptureDeviceInput和AVCaptureVideoDataOutput,也就是管理輸入輸出過程,因此稱做Session。相機的輸入配置就是AVCaptureDeviceInput,主要解決是否使用自動曝光、自動白平衡之類的,而輸出配置就是AVCaptureVideoDataOutput,主要決定輸出視頻圖像的格式之類的。AVCaptureDevice表示捕捉設備,由於具體捕獲的內容不明確,因此還會區分捕捉視頻的設備仍是捕捉聲音的設備。這裏咱們從捕捉這個詞能夠看出其實AVCaptureDevice和輸入AVCaptureDeviceInput關係緊密。atom

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,就能夠在函數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);
}

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-參考資料


相關文章
相關標籤/搜索