随着前有Apple在iOS11中提供了ARKit,后有Google推出的ARCore,显然掀起了一股AR热潮(都是一堆废话,说白了就是公司要求做)。
  
  由于Vuforia已经存在较长时间了,相对于EasyAR或者百度AR更为成熟一点儿,所以它成了第一个技术选择。EasyAR和百度的AR——DuMix AR后面再依次去学习。好吧,先开始来学习Vuforia吧!
该文章同步发布在我的博客.

集成步骤

由于这些步骤相对来说比较基础,我就直接罗列出来。

  • 1、开发者官网https://developer.vuforia.com;

  • 2、下载ios-sdk和ios-sample,并按照官方文档要求将ios-sample放入到ios-sdk的sample文件夹下

  • 3、在vuforia的开发者官网上的License Manager和Target Manager,添加License-key和database
    License-Key

  • 4、将上一步添加的License-key,放在ios-sample中的代码文件SampleApplicationSession.mm中的方法Vuforia::setInitParameters(mVuforiaInitFlags,"");中。具体步骤见官网

    Vuforia::setInitParameters(mVuforiaInitFlags,"AT16FIX/////AAAAGVieZ/kg1UkghTnYAz5zXWs8+y5JjeF/NJRcjgVDoCSvsrSt+lWzFMcIVBbQ2YSFRF+6J0GceHoaz8NctXib3cndJEacXmR+1FyO5FhalO7sC4hE9d1/x72qTNDhkPs4rF04JulMYT876Grsnmg9C61oyaDVwBfSpzNZ7gx3NADkkV5q4NQs4ghZwVCdMhj6LVt1YTJcwiuULtDTEgpFZZeW/nC8yiC53hpUFOVxhH++ILx1T65jpY8yDn6ct++3mgVeVotg/5tWXYb5FYqBtJiwU/LJJxhJYqUWyy4pd9dHUJBQojuAE8FoW1DmjokrpDWgjOMMp3am4GjNT04hCg+o0Z3SByYx6VIqfSR9fsXw");
    
  • 5、将第3步中Target Manager创建的database,传入相关的图片文件。也可以不传,这步可选;怎样使用设备数据。我们在官方Developer创建,下载对应的Database,在添加图片时,对应的星星数越高表明识别度越高
    Target-Manager
    解压并将其引入到工程中:
    引入工程
    这一步可以不做,因为这只是在给后面打基础而已,如果只是运行demo的话是不需要做这一步的。如果做了这一步,在扫描你对应的图片的时候是没有任何效果的,具体的操作后面。

  • 6、编译运行,由于需要使用到传感器,所以必须使用真机来运行。关于真机运行的相关事项查看apple developer

源码阅读顺序

源码说明:
Voforia SDK版本:vuforia-sdk-ios-6-5-19
iOS Samples版本:vuforia-samples-core-ios-6-5-20

如果不想看源码相关可直接跳过这部分,直接跟着“收尾”做自定义的tracker和模型(替换Teapot茶壶模型为自己的)

为何要阅读源码?因为在Voforia的官方文档中我没有找到我自己想要的信息。所以我们需要通过阅读源码,来找到怎样才能去修改贴在目标图像中虚拟模型。以sample中ImageTargetsViewController为例来解读!
首先查看ImageTargetsViewController.h文件,我们先不看成员变量。先来看属性

属性 属性类型 初步作用
eaglView ImageTargetsEAGLView* 初步认定为一个展示视图
tapGestureRecognizer UITapGestureRecognizer* 一个点击手势
vapp SampleApplicationSession* 初步认定为一个会话层(类似于ISO网络七层模型中,在TCP可以归于应用层,也就是说想偷懒可以直接将其代码放入控制器中。个人理解)
showingMenu BOOL 一个flag

从上表中出现的属性,我们先来分析一下属性eaglViewvapp

SampleApplicationSession类

ImageTargetsViewController控制器类中,和下面会讲到的ImageTargetsEAGLView都有SampleApplicationSession类型的属性,所以我们有必要先来看看该类。同样的先看头文件,因为头文件能够让我们对于该类有大体的认识,而不拘于类具体的实现细节。
  粗略来看,提供了一个初始化方法;一个初始化AR的方法;四个对AR的操作方法(它们不是我们需要的重点,等到需要的时候再来仔细阅读);以及一个对Camera的方法:

1
2
3
4
5
6
7
- (id)initWithDelegate:(id<SampleApplicationControl>) delegate;
- (void) initAR:(int) VuforiaInitFlags orientation:(UIInterfaceOrientation) ARViewOrientation;
- (bool) startAR:(Vuforia::CameraDevice::CAMERA_DIRECTION) camera error:(NSError **)error;
- (bool) pauseAR:(NSError **)error;
- (bool) resumeAR:(NSError **)error;
- (bool) stopAR:(NSError **)error;
- (bool) stopCamera:(NSError **)error;

上述的initAR方法是通过异步实现的,当其AR初始化完成之后会调用方法下面会提到的代理方法onInitARDone。顺藤摸瓜,我们来看看该代理,那么该代理所需要处理的事务有哪些呢?这里先将SampleApplicationControl的所有方法先列出来:

1
2
3
4
5
6
7
8
9
10
11
12
@required
- (void) onInitARDone:(NSError *)error;
- (bool) doInitTrackers;
- (bool) doLoadTrackersData;
- (bool) doStartTrackers;
- (bool) doStopTrackers;
- (bool) doUnloadTrackersData;
- (bool) doDeinitTrackers;
- (void)configureVideoBackgroundWithViewWidth:(float)viewWidth andHeight:(float)viewHeight;
@optional
- (void) onVuforiaUpdate: (Vuforia::State *) state;

该代理方法中大多是涉及到的是tracker。通过从初始化方法开始查看方法调用,得出了一个程序执行流程图,
图-1
我们主动调用initAR方法,其结果会由回调方法onInitARDone反应给开发者。开发者可以用通过调用doInitTrackers来控制是否需要去加载tracker数据,如果可以加载数据则通过调用回调方法doLoadTrackersData来获取数据。关于该类中其他几个方法startAR , pauseAR, resumeAR, stopAR由调用人员主动调用,调用这些方法会触发对应的方法回调。
现在我们需要把目光转向ImageTargetsEAGLView类,并去具体的看一下里面的相关细节。

SampleAppRenderer类

这个类主要是做渲染相关的工作,其源码大多数为OpenGL。所以对于该类我只做具体的作用分析,而不去解释具体的源代码(因为我也不懂),如有需要的话,自行深究吧,哈哈哈😄。这里先将各个方法的作用罗列出来:

方法名 方法作用
initWithSampleAppRendererControl 类初始化方法
initRendering 渲染相关的初始化
setNearPlane:farPlane: 配置投影矩阵数据
renderFrameVuforia 由Vuforia调用,渲染数据帧到屏幕
renderVideoBackground 后台渲染视频
configureVideoBackgroundWithViewWidth:andHeight: 视频相关的配置
updateRenderingPrimitives 更新渲染数据

下面具体分析:老规矩,同样先看头文件,我们根据头文件暴露出来的方法一层一层往里剥。该类存在一个协议SampleAppRendererControl,和一个初始化方法initWithSampleAppRendererControl。使用这个方法需要传入一个遵守SampleAppRendererControl协议的类实例,第二个参数来决定VR/AR的模式,以及三个用于决定投影矩阵的参数。除了在初始化方法设置投影矩阵的参数,该类提供了一个public方法setNearPlane:farPlane:。进入到.mm文件中查看该初始化方法可以看出,只是对类内部私有属性进行相关的赋值操作以及对硬件设备进行相关的设置吧。
  现在来看看方法initRendering,这个方法里面主要是做了一些OpenGLES的东西,我们只需要知道里面做了一些和具体业务逻辑无关的东西就行了。
  接下来看renderFrameVuforia的作用是什么?源代码中说的很清楚:使用OpenGL绘制当前帧,当需要将当前帧渲染到屏幕上时,Vuforia会定期的在后台线程调用该方法。同样和业务逻辑无关,源码不细看。同样方法renderVideoBackground也是使用OpenGL来做,我们只需要从该方法的名字得知其用途(后台渲染视频)即可。
  configureVideoBackgroundWithViewWidth:andHeight:方法从名字就可以知道其作用。updateRenderingPrimitives方法的作用是:当屏幕尺寸发生改变或者是设备朝向改变之后,调用该方法来更新渲染原始数据。
  最后需要介绍一下该类很重要的的一个协议方法:renderFrameWithState,该方法被用于获取渲染相关的数据。通过对.mm文件可知,每渲染一次都会调用该方法一次。

ImageTargetsEAGLView类

该类的头文件所暴露出来的初始化方法- (id)initWithFrame:(CGRect)frame appSession:(SampleApplicationSession *) app,我们以该方法入手来分析。第一个参数为当前视图的大小设置,第二个参数为前面我们讲到过的一个类实例。头文件中余下的方法还有:

1
2
3
4
5
6
- (void)finishOpenGLESCommands;
- (void)freeOpenGLESResources;
- (void) setOffTargetTrackingMode:(BOOL) enabled;
- (void) configureVideoBackgroundWithViewWidth:(float)viewWidth andHeight:(float)viewHeight;
- (void) updateRenderingPrimitives;

方法finishOpenGLESCommands , freeOpenGLESResources分别对应着结束OpenGL和释放OpenGL的资源。configureVideoBackgroundWithViewWidth:andHeight: , updateRenderingPrimitives和类SampleAppRenderer公开的方法名一样,这里我猜测它们作用是一样的。setOffTargetTrackingMode:方法作用目前还不是很清晰,需要去.mm文件中详查。
  现在进入实现文件中,源码中提到了关于OpenGL线程安全的问题。iOS上的OpenGL ES是线程不安全的,在程序中Vuforia使用下面的方法来保证线程(OpenGL 上下文)安全:

  • a、在主线程中创建OpenGL ES上下文。
  • b、Vuforia相机开始时,将其位于我们自己EAGLView视图上,并开启renderer线程。
  • c、Vuforia会在renderer线程上,定期调用我们的renderFrameVuforia(SampleAppRenderer类提到)方法。当第一次调用该方法的时候,defaultFramebuffer并不存在,调用createFramebuffer方法来创建它。createFramebuffer由主线程调用,而与此同时renderer线程会被阻塞。因此确保OpenGL ES上下文不会被并行使用

在initWithFrame:appSession:的实现方法中会进行session,OpenGL的context和Renderer的赋值,初始化和绑定工作。而方法configureVideoBackgroundWithViewWidth:andHeight: , updateRenderingPrimitives在其实现方法中的确只是简单的调用了一下SampleAppRenderer 类的实例方法。
  现在主要来看看方法setOffTargetTrackingMode :,它的实现很简单只是对其私有成员变量NO。但是却在协议方法renderFrameWithState中大量的使用。该方法大部分是OpenGL相关的工作,我没有深究下去,只整理出来一个工作流程图:
图-2
目前来看ImageTargetsEAGLView类的主要作用在于保证OpenGL在iOS中达到线程安全,创建buffer和对buffer的管理,提供了对OpenGL的控制,而实际的渲染则由SampleAppRenderer来实现。

ImageTargetsViewController类

现在将目光回到ImageTargetsViewController类上面来。由于是一个控制器类,所以我直接从.mm文件中着手。根据ViewController的加载顺序来看具体的逻辑,首先查找loadView方法,如果没有则查找viewDidLoad。源码中,loadView方法主要创建了vapp,eaglView以及对vapp初始化了AR相关的事务(其他视图和手势等先忽略,只关心属性vapp,eaglView相关的逻辑),将ViewController的View设置为eaglView。
  在loadView中将vapp的代理设置为控制器自身,此时通过上面介绍 SampleApplicationSession时对应的程序执行流程,将目光放在对应的部分协议方法上面。

1
2
3
4
5
6
@protocol SampleApplicationControl
- (void) onInitARDone:(NSError *)error;
- (bool) doInitTrackers;
- (bool) doLoadTrackersData;
- (bool) doStartTrackers;
@end

从图-1可以看出是由doInitTrackers的返回值来判断是否需要去加载tracker数据(doLoadTrachersData),最后在onInitARDone方法流程结束。通过这个就确定了我们的源码查看顺序:

1
/// doInitTrackers --> doLoadTrackersData --> onInitARDone

那么在ImageTargetsViewController类中,其流程图如下:
图-3
自此我们的源码阅读就告一段落,最后我们将要去实现开始提到的目的!

收尾

读到这里,自定义的数据集的切入点在方法doLoadTrackersData方法中,并且要doInitTrackers方法返回YES。如果没有执行“安装步骤”中的第5步的话,现在可以去做了!做完之后添加如下代码到工程中:

1
2
3
4
5
6
7
8
///ImageTargetsViewController.mm -> doLoadTrackersData
dataSetCustom = [self loadObjectTrackerDataSet:@"WillDB_Device.xml"];/// 这个dataset为你自己的名字
if (dataSetCustom == NULL) {
return NO;
}
if (! [self activateDataSet:dataSetCustom]) {
return NO;
}

运行程序,扫描对应的图片发现是能够成功扫描对应的图片的。但是系统的图片能出来一个“茶壶”,而我们自己的图片上面什么也没有呢?
在这里我不想又去使用这个烦人的“茶壶”OpenGL模型了,我选择的是一个皮卡丘的原型(在文末我会将改造过的demo传到Github可以去那里下载这个原型)。

模型obj到opengl数据的转换

就目前我知道的来说,在Xcode中无法使用.obj的模型数据的。我在网上找到了一个工具obj2opengl,具体的使用方法见这里,我还是大体来说一下使用步骤:
将下载好的文件放到特定的文件夹中,然后把对应的obj文件和它放在一起,使用终端进入obj2opengl.pl文件所在文件夹之后,输入如下命令:

1
./obj2opengl.pl yourobjfilename

成功后,它会生成一个头文件,这就是通过obj文件生成的纹理坐标代码,在该头文件中有3个数组,这三个数组分别对应着xxxVerts [], xxxNormals [], xxxTexCoords [],和一个xxxxNumVerts(xxx为你的obj文件名字),具体使用说明

模型替换

通过前面的源码阅读,我们知道ImageTargetsViewController类是用来加载tracker数据的,SampleAppRenderer类是做渲染相关的数据,SampleApplicationSession类是使用tracker数据并控制AR,最后只剩下一个ImageTargetsEAGLView类。在该类中会做如下操作:

  • 1、在textureFilenames数组中,添加一个新的纹理。这个自己选择一个纹理图片,我是随便选的,所以看起来会很丑。
  • 2、在ImageTargetsEAGLView类的头文件中添加一个私有成员变量pikachuModel。用它来代替例子中的buildingModel。并在SampleApplication3DModel.h文件中添加方法pikachu_ReWrite,并在SampleApplication3DModel.m文件中添加如下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    - (void)pikachu_ReWrite{
    #if kUse3DModel == 1
    _numVertices = XY_PikachuMNumVerts;
    _vertices = XY_PikachuMVerts;
    _normals = XY_PikachuMNormals;
    _texCoords = XY_PikachuMTexCoords;
    #endif
    }
  • 3、在ImageTargetsEAGLView.mm的方法loadBuildingsModel中添加如下代码:

    1
    2
    pikachuModel = [[SampleApplication3DModel alloc] init];
    [pikachuModel pikachu_ReWrite];
  • 4、将ImageTargetsEAGLView.mm文件中所有的buildingModel替换为pikachuModel,最后调节一下变量kObjectScaleOffTargetTracking的值,这个值调节由自己决定。

上述的修改灵感大多是来自头文件Teapot.h,但是我们使用obj2opengl时生成的文件中并没有Teapot.h中teapotIndices对应的数组。相反多了一个无符号的整形变量xxxNumVerts,所以除了上诉的方法以外还有另外一种方法,具体的代码修改如下:

1
2
3
4
5
6
/// ImageTargetsEAGLView.mm -> renderFrameWithState方法中
glVertexPointer(3, GL_FLOAT, 0, XY_PikachuMVerts);
glNormalPointer(GL_FLOAT, 0, XY_PikachuMNormals);
glTexCoordPointer(2, GL_FLOAT, 0, XY_PikachuMTexCoords);
glDrawArrays(GL_TRIANGLES, 0, XY_PikachuMNumVerts);

在使用这个方法时其(用obj2opengl生成的头文件的数组中的)数值比例是需要修改的,而且还需要对模型进行翻转。这个方法具体见How do I replace the TeapotReplace the teapot model
在修改源码时主要就是在修改方法renderFrameWithState,它在介绍ImageTargetsEAGLView类时在文件的最开头就有提到,它是在每捕捉到一次tracker之后就会运行一次。
到这里一个很基本的Vuforia集成,源码的解读以及自定义tracker和模型就算完成了,最后附上Demo地址

相关链接