0x00 - 前言


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

所以说要在手机上进行现实场景的展现也是目前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的绘制。

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对象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, ); // 获取图像缓存区的宽高
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, ); // 绘制部分
// ...
}

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, ); // 获取图像缓存区的宽高
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(, &_defaultFrameBuffer);
glGenRenderbuffersOES(, &_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: height:];
} return self;
} #pragma mark - private methods
// 构建一个宽width高height的纹理对象
- (GLuint)genTexWithWidth:(GLuint)width height:(GLuint)height
{
GLuint texId;
// 生成并绑定纹理对象
glGenTextures(, &texId);
glBindTexture(GL_TEXTURE_2D, texId);
// 注意这里纹理的像素格式为GL_RGBA
glTexImage2D(GL_TEXTURE_2D, , GL_RGBA, width, height, , 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, ); return texId;
} #pragma mark - ESRenderProtocol
- (void)render
{
// 设置绘制上下文
[EAGLContext setCurrentContext:_context];
glBindFramebufferOES(GL_FRAMEBUFFER_OES, _defaultFrameBuffer); // 相机纹理坐标
static GLfloat spriteTexcoords[] = {
,,
,,
,,
,};
// 相机顶点坐标
static GLfloat spriteVertices[] = {
,,
,,
,,
,}; // 清除颜色缓存
glClearColor(0.0, 0.0, 0.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
// 视口矩阵
glViewport(, , _backingWidth, _backingHeight);
// 投影矩阵
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
// 正投影
glOrthof(, , _backingHeight*/_backingWidth, , , ); // 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(, GL_FLOAT, , spriteVertices);
// 将spriteTexcoords传递到纹理坐标数组中
glTexCoordPointer(, GL_FLOAT, , spriteTexcoords);
// 将camTexId纹理对象绑定到2D纹理
glBindTexture(GL_TEXTURE_2D, _camTexId);
// 根据videoBuffer获取imgMat(相机图像)
glTexSubImage2D(GL_TEXTURE_2D, , , , , , GL_RGBA, GL_UNSIGNED_BYTE, _videoBuffer.imgMat.data);
// 绘制纹理
glDrawArrays(GL_TRIANGLE_STRIP, , ); // 解绑2D纹理
glBindTexture(GL_TEXTURE_2D, );
// 与上面的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。所以为了让图像全部能显示在屏幕上,我选择了等宽显示。

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

0x05-参考资料


【AR实验室】OpenGL ES绘制相机(OpenGL ES 1.0版本)的更多相关文章

  1. es学习-java操作 2.4.0版本

    package esjava; import org.elasticsearch.action.bulk.*;import org.elasticsearch.action.delete.Delete ...

  2. 【AR实验室】mulberryAR : ORBSLAM2+VVSION

    本文转载请注明出处 —— polobymulberry-博客园 0x00 - 前言 mulberryAR是我业余时间弄的一个AR引擎,目前主要支持单目视觉SLAM+3D渲染,并且支持iOS端,但是该引 ...

  3. 使用OpenGL ES绘制3D图形

    如果应用定义的顶点不在同一个平面上,并且使用三角形把合适的顶点连接起来,就可以绘制出3D图形了. 使用OpenGL  ES绘制3D图形的方法与绘制2D图形的步骤大致相同,只是绘制3D图形需要定义更多的 ...

  4. 【Qt for Android】OpenGL ES 绘制彩色立方体

    Qt 内置对OpenGL ES的支持.选用Qt进行OpenGL ES的开发是很方便的,很多辅助类都已经具备.从Qt 5.0開始添加了一个QWindow类,该类既能够使用OpenGL绘制3D图形,也能够 ...

  5. Android OpenGL ES 开发(四): OpenGL ES 绘制形状

    在上文中,我们使用OpenGL定义了能够被绘制出来的形状了,现在我们想绘制出来它们.使用OpenGLES 2.0来绘制形状会比你想象的需要更多的代码.因为OpenGL的API提供了大量的对渲染管线的控 ...

  6. [转]关于OpenGL的绘制上下文

    [转]关于OpenGL的绘制上下文 本文转自(http://www.cnblogs.com/Liuwq/p/5444641.html) 什么是绘制上下文(Rendering Context) 初学Op ...

  7. CSharpGL(6)在OpenGL中绘制UI元素

    CSharpGL(6)在OpenGL中绘制UI元素 2016-08-13 由于CSharpGL一直在更新,现在这个教程已经不适用最新的代码了.CSharpGL源码中包含10多个独立的Demo,更适合入 ...

  8. OpenGL学习-------绘制简单的几何图形

    本次课程所要讲的是绘制简单的几何图形,在实际绘制之前,让我们先熟悉一些概念. 一.点.直线和多边形我们知道数学(具体的说,是几何学)中有点.直线和多边形的概念,但这些概念在计算机中会有所不同.数学上的 ...

  9. tao.opengl+C#绘制三维模型

    一.tao.Opengl技术简介 Opengl是一种C风格的图形库,即opengl中没有类和对象,只有大量的函数.Opengl在内部就是一个状态机,利用不同的函数来修改opengl状态机的状态,以达到 ...

随机推荐

  1. nodejs模块发布及命令行程序开发

    前置技能 npm工具为nodejs提供了一个模块和管理程序模块依赖的机制,当我们希望把模块贡献出去给他人使用时,可以把我们的程序发布到npm提供的公共仓库中,为了方便模块的管理,npm规定要使用一个叫 ...

  2. SignalR SelfHost实时消息,集成到web中,实现服务器消息推送

    先前用过两次SignalR,但是中途有段时间没弄了,今天重新弄,发现已经忘得差不多了,做个笔记! 首先创建一个控制台项目Nuget添加引用联机搜索:Microsoft.AspNet.SignalR.S ...

  3. 算法与数据结构(十五) 归并排序(Swift 3.0版)

    上篇博客我们主要聊了堆排序的相关内容,本篇博客,我们就来聊一下归并排序的相关内容.归并排序主要用了分治法的思想,在归并排序中,将我们需要排序的数组进行拆分,将其拆分的足够小.当拆分的数组中只有一个元素 ...

  4. 算法与数据结构(九) 查找表的顺序查找、折半查找、插值查找以及Fibonacci查找

    今天这篇博客就聊聊几种常见的查找算法,当然本篇博客只是涉及了部分查找算法,接下来的几篇博客中都将会介绍关于查找的相关内容.本篇博客主要介绍查找表的顺序查找.折半查找.插值查找以及Fibonacci查找 ...

  5. DBA成长路线

    从开发转为数据库管理,即人们称为DBA的已经有好几年,有了与当初不一样的体会.数据是企业的血液,数据是石油,数据是一切大数据.云计算的基础.作为DBA是数据的保卫者.管理者,是企业非常重要的角色.对于 ...

  6. 界面设计技法之css布局

    css布局之于页面就如同ECMAScript之于JS一般,细想一番,html就如同语文,css就如同数学,js呢,就是物理,有些扯远,这里就先不展开了. 回到主题,从最开始的css到如今的sass(l ...

  7. webpack学习总结

    前言 在还未接触webpack,就有几个疑问: 1. webpack本质上是什么? 2. 跟异步模块加载有关系吗? 3. 可否生成多个文件,一定是一个? 4. 被引用的文件有其他异步加载模块怎么办? ...

  8. Maven常用命令

    开发中常用的命令: 1. mvn compile 编译源代码2. mvn test-compile 编译测试代码3. mvn test 运行测试4. mvn package 打包,根据pom.xml打 ...

  9. Atitit  godaddy 文件权限 root权限设置

    Atitit  godaddy 文件权限 root权限设置 1. ubuntu需要先登录,再su切换到root1 2. sudo 授权许可使用的su,也是受限制的su1 3. ubuntu默认吗roo ...

  10. 使用Nginx反向代理 让IIS和Tomcat等多个站点一起飞

    使用Nginx 让IIS和Tomcat等多个站点一起飞 前言: 养成一个好习惯,解决一个什么问题之后就记下来,毕竟“好记性不如烂笔头”. 这样也能帮助更多的人 不是吗? 最近闲着没事儿瞎搞,自己在写一 ...