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版本)的更多相关文章
-
es学习-java操作 2.4.0版本
package esjava; import org.elasticsearch.action.bulk.*;import org.elasticsearch.action.delete.Delete ...
-
【AR实验室】mulberryAR : ORBSLAM2+VVSION
本文转载请注明出处 —— polobymulberry-博客园 0x00 - 前言 mulberryAR是我业余时间弄的一个AR引擎,目前主要支持单目视觉SLAM+3D渲染,并且支持iOS端,但是该引 ...
-
使用OpenGL ES绘制3D图形
如果应用定义的顶点不在同一个平面上,并且使用三角形把合适的顶点连接起来,就可以绘制出3D图形了. 使用OpenGL ES绘制3D图形的方法与绘制2D图形的步骤大致相同,只是绘制3D图形需要定义更多的 ...
-
【Qt for Android】OpenGL ES 绘制彩色立方体
Qt 内置对OpenGL ES的支持.选用Qt进行OpenGL ES的开发是很方便的,很多辅助类都已经具备.从Qt 5.0開始添加了一个QWindow类,该类既能够使用OpenGL绘制3D图形,也能够 ...
-
Android OpenGL ES 开发(四): OpenGL ES 绘制形状
在上文中,我们使用OpenGL定义了能够被绘制出来的形状了,现在我们想绘制出来它们.使用OpenGLES 2.0来绘制形状会比你想象的需要更多的代码.因为OpenGL的API提供了大量的对渲染管线的控 ...
-
[转]关于OpenGL的绘制上下文
[转]关于OpenGL的绘制上下文 本文转自(http://www.cnblogs.com/Liuwq/p/5444641.html) 什么是绘制上下文(Rendering Context) 初学Op ...
-
CSharpGL(6)在OpenGL中绘制UI元素
CSharpGL(6)在OpenGL中绘制UI元素 2016-08-13 由于CSharpGL一直在更新,现在这个教程已经不适用最新的代码了.CSharpGL源码中包含10多个独立的Demo,更适合入 ...
-
OpenGL学习-------绘制简单的几何图形
本次课程所要讲的是绘制简单的几何图形,在实际绘制之前,让我们先熟悉一些概念. 一.点.直线和多边形我们知道数学(具体的说,是几何学)中有点.直线和多边形的概念,但这些概念在计算机中会有所不同.数学上的 ...
-
tao.opengl+C#绘制三维模型
一.tao.Opengl技术简介 Opengl是一种C风格的图形库,即opengl中没有类和对象,只有大量的函数.Opengl在内部就是一个状态机,利用不同的函数来修改opengl状态机的状态,以达到 ...
随机推荐
-
Vertica增加一个数据存储的目录
Vertica增加一个数据存储的目录 操作语法为: ADD_LOCATION ( 'path' , [ 'node' , 'usage', 'location_label' ] ) 各节点添加目录,并 ...
-
关于WPF中ItemsControl系列控件中Item不能继承父级的DataContext的解决办法
WPF中所有的集合类控件,子项都不能继承父级的DataContext,需要手动将绑定的数据源指向到父级控件才可以. <DataGridTemplateColumn Header="操作 ...
-
[Python] 中文编码问题:raw_input输入、文件读取、变量比较等str、unicode、utf-8转换问题
最近研究搜索引擎.知识图谱和Python爬虫比较多,中文乱码问题再次浮现于眼前.虽然市面上讲述中文编码问题的文章数不胜数,同时以前我也讲述过PHP处理数据库服务器中文乱码问题,但是此处还是准备简单做下 ...
-
烂泥:LVM学习之KVM利用LVM快照备份与恢复虚拟机
本文由秀依林枫提供友情赞助,首发于烂泥行天下. 最近一段时间一直在学习有关LVM逻辑卷方面的知识,前几篇文章介绍了有关LVM的逻辑卷的基本相关知识,包括逻辑卷及卷组的扩容与缩小.今天我们再来介绍LVM ...
-
hihocoder 1388 &;&;2016 ACM/ICPC Asia Regional Beijing Online Periodic Signal
#1388 : Periodic Signal 时间限制:5000ms 单点时限:5000ms 内存限制:256MB 描述 Profess X is an expert in signal proce ...
-
ncdu 磁盘目录查看工具
我平时都是直接yum -y install ncdu,但是今天失败了. 所以: 安装EPEL源 CentOS/RHEL 5 : rpm -Uvh https://dl.fedoraproject.or ...
-
ECshop中TemplateBeginEditable 和后台编辑讲解
在ecshop的dwt文件里面经常发现有“<!-- TemplateBeginEditable name="doctitle" -->和<!-- #BeginLi ...
-
ios coreData使用
ios中的coredata的使用(转) 分类: ios2013-07-15 18:12 27288人阅读 评论(1) 收藏 举报 Core Data数据持久化是对SQLite的一个升级,它是ios集成 ...
-
SQL中的模糊查询
写个标题先.先来一篇大神的文章:http://www.cnblogs.com/GT_Andy/archive/2009/12/25/1921914.html 练习代码如下: 1.百分号:% 表示任 ...
-
Raknet实现的简单服务器与客户端的交互
1. 首先下载Raknet的源代码,我用的是4.0的,不是最新的,解压后编译DLL工程,编译完成后进入解压的根目录下,进入Lib文件夹下找到RakNet_DLL_Debug_Win32.dll, R ...