
??隨著前有Apple在iOS11中提供了ARKit,后有Google推出的ARCore,顯然掀起了一股AR熱潮(都是一堆廢話,說白了就是公司要求做)。由于高通的Vuforia已經(jīng)存在較長時間了,相對于EasyAR或者百度AR更為成熟一點兒,所以它成了第一個技術(shù)選擇。EasyAR和百度的AR——DuMix AR后面再依次去學(xué)習(xí)。好吧,先開始來學(xué)習(xí)Vuforia吧!
該文章同步發(fā)布在我的博客.
集成步驟
由于這些步驟相對來說比較基礎(chǔ),我就直接羅列出來。
1、開發(fā)者官網(wǎng)https://developer.vuforia.com;
2、下載ios-sdk和ios-sample,并按照官方文檔要求將ios-sample放入到ios-sdk的sample文件夾下
-
3、在vuforia的開發(fā)者官網(wǎng)上的License Manager和Target Manager,添加License-key和database
License-Key -
4、將上一步添加的License-key,放在ios-sample中的代碼文件SampleApplicationSession.mm中的方法
Vuforia::setInitParameters(mVuforiaInitFlags,"");中。具體步驟見官網(wǎng)。Vuforia::setInitParameters(mVuforiaInitFlags,"AT16FIX/////AAAAGVieZ/kg1UkghTnYAz5zXWs8+y5JjeF/NJRcjgVDoCSvsrSt+lWzFMcIVBbQ2YSFRF+6J0GceHoaz8NctXib3cndJEacXmR+1FyO5FhalO7sC4hE9d1/x72qTNDhkPs4rF04JulMYT876Grsnmg9C61oyaDVwBfSpzNZ7gx3NADkkV5q4NQs4ghZwVCdMhj6LVt1YTJcwiuULtDTEgpFZZeW/nC8yiC53hpUFOVxhH++ILx1T65jpY8yDn6ct++3mgVeVotg/5tWXYb5FYqBtJiwU/LJJxhJYqUWyy4pd9dHUJBQojuAE8FoW1DmjokrpDWgjOMMp3am4GjNT04hCg+o0Z3SByYx6VIqfSR9fsXw"); -
5、將第3步中Target Manager創(chuàng)建的database,傳入相關(guān)的圖片文件。也可以不傳,這步可選;怎樣使用設(shè)備數(shù)據(jù)。我們在官方Developer創(chuàng)建,下載對應(yīng)的Database,在添加圖片時,對應(yīng)的星星數(shù)越高表明識別度越高
Target-Manager
解壓并將其引入到工程中:
引入工程
這一步可以不做,因為這只是在給后面打基礎(chǔ)而已,如果只是運行demo的話是不需要做這一步的。如果做了這一步,在掃描你對應(yīng)的圖片的時候是沒有任何效果的,具體的操作后面。 6、編譯運行,由于需要使用到傳感器,所以必須使用真機來運行。關(guān)于真機運行的相關(guān)事項查看apple developer
源碼閱讀順序
源碼說明:
Voforia SDK版本:vuforia-sdk-ios-6-5-19
iOS Samples版本:vuforia-samples-core-ios-6-5-20
如果不想看源碼相關(guān)可直接跳過這部分,直接跟著“收尾”做自定義的tracker和模型(替換Teapot茶壺模型為自己的)
為何要閱讀源碼?因為在Voforia的官方文檔中我沒有找到我自己想要的信息。所以我們需要通過閱讀源碼,來找到怎樣才能去修改貼在目標圖像中虛擬模型。以sample中ImageTargetsViewController為例來解讀!
首先查看ImageTargetsViewController.h文件,我們先不看成員變量。先來看屬性
| 屬性 | 屬性類型 | 初步作用 |
|---|---|---|
| eaglView | ImageTargetsEAGLView* |
初步認定為一個展示視圖 |
| tapGestureRecognizer | UITapGestureRecognizer* |
一個點擊手勢 |
| vapp | SampleApplicationSession* |
初步認定為一個會話層(類似于ISO網(wǎng)絡(luò)七層模型中,在TCP可以歸于應(yīng)用層,也就是說想偷懶可以直接將其代碼放入控制器中。個人理解) |
| showingMenu | BOOL |
一個flag |
從上表中出現(xiàn)的屬性,我們先來分析一下屬性eaglView和vapp。
SampleApplicationSession類
在ImageTargetsViewController控制器類中,和下面會講到的ImageTargetsEAGLView都有SampleApplicationSession類型的屬性,所以我們有必要先來看看該類。同樣的先看頭文件,因為頭文件能夠讓我們對于該類有大體的認識,而不拘于類具體的實現(xiàn)細節(jié)。
??粗略來看,提供了一個初始化方法;一個初始化AR的方法;四個對AR的操作方法(它們不是我們需要的重點,等到需要的時候再來仔細閱讀);以及一個對Camera的方法:
- (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方法是通過異步實現(xiàn)的,當其AR初始化完成之后會調(diào)用方法下面會提到的代理方法onInitARDone。順藤摸瓜,我們來看看該代理,那么該代理所需要處理的事務(wù)有哪些呢?這里先將SampleApplicationControl的所有方法先列出來:
@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。通過從初始化方法開始查看方法調(diào)用,得出了一個程序執(zhí)行流程圖,

我們主動調(diào)用
initAR方法,其結(jié)果會由回調(diào)方法onInitARDone反應(yīng)給開發(fā)者。開發(fā)者可以用通過調(diào)用doInitTrackers來控制是否需要去加載tracker數(shù)據(jù),如果可以加載數(shù)據(jù)則通過調(diào)用回調(diào)方法doLoadTrackersData來獲取數(shù)據(jù)。關(guān)于該類中其他幾個方法startAR , pauseAR, resumeAR, stopAR由調(diào)用人員主動調(diào)用,調(diào)用這些方法會觸發(fā)對應(yīng)的方法回調(diào)。現(xiàn)在我們需要把目光轉(zhuǎn)向
ImageTargetsEAGLView類,并去具體的看一下里面的相關(guān)細節(jié)。
SampleAppRenderer類
這個類主要是做渲染相關(guān)的工作,其源碼大多數(shù)為OpenGL。所以對于該類我只做具體的作用分析,而不去解釋具體的源代碼(因為我也不懂),如有需要的話,自行深究吧,哈哈哈??。這里先將各個方法的作用羅列出來:
| 方法名 | 方法作用 |
|---|---|
| initWithSampleAppRendererControl | 類初始化方法 |
| initRendering | 渲染相關(guān)的初始化 |
| setNearPlane:farPlane: | 配置投影矩陣數(shù)據(jù) |
| renderFrameVuforia | 由Vuforia調(diào)用,渲染數(shù)據(jù)幀到屏幕 |
| renderVideoBackground | 后臺渲染視頻 |
| configureVideoBackgroundWithViewWidth:andHeight: | 視頻相關(guān)的配置 |
| updateRenderingPrimitives | 更新渲染數(shù)據(jù) |
下面具體分析:老規(guī)矩,同樣先看頭文件,我們根據(jù)頭文件暴露出來的方法一層一層往里剝。該類存在一個協(xié)議SampleAppRendererControl,和一個初始化方法initWithSampleAppRendererControl。使用這個方法需要傳入一個遵守SampleAppRendererControl協(xié)議的類實例,第二個參數(shù)來決定VR/AR的模式,以及三個用于決定投影矩陣的參數(shù)。除了在初始化方法設(shè)置投影矩陣的參數(shù),該類提供了一個public方法setNearPlane:farPlane:。進入到.mm文件中查看該初始化方法可以看出,只是對類內(nèi)部私有屬性進行相關(guān)的賦值操作以及對硬件設(shè)備進行相關(guān)的設(shè)置吧。
??現(xiàn)在來看看方法initRendering,這個方法里面主要是做了一些OpenGLES的東西,我們只需要知道里面做了一些和具體業(yè)務(wù)邏輯無關(guān)的東西就行了。
??接下來看renderFrameVuforia的作用是什么?源代碼中說的很清楚:使用OpenGL繪制當前幀,當需要將當前幀渲染到屏幕上時,Vuforia會定期的在后臺線程調(diào)用該方法。同樣和業(yè)務(wù)邏輯無關(guān),源碼不細看。同樣方法renderVideoBackground也是使用OpenGL來做,我們只需要從該方法的名字得知其用途(后臺渲染視頻)即可。
??configureVideoBackgroundWithViewWidth:andHeight:方法從名字就可以知道其作用。updateRenderingPrimitives方法的作用是:當屏幕尺寸發(fā)生改變或者是設(shè)備朝向改變之后,調(diào)用該方法來更新渲染原始數(shù)據(jù)。
??最后需要介紹一下該類很重要的的一個協(xié)議方法:renderFrameWithState,該方法被用于獲取渲染相關(guān)的數(shù)據(jù)。通過對.mm文件可知,每渲染一次都會調(diào)用該方法一次。
ImageTargetsEAGLView類
該類的頭文件所暴露出來的初始化方法- (id)initWithFrame:(CGRect)frame appSession:(SampleApplicationSession *) app,我們以該方法入手來分析。第一個參數(shù)為當前視圖的大小設(shè)置,第二個參數(shù)為前面我們講到過的一個類實例。頭文件中余下的方法還有:
- (void)finishOpenGLESCommands;
- (void)freeOpenGLESResources;
- (void) setOffTargetTrackingMode:(BOOL) enabled;
- (void) configureVideoBackgroundWithViewWidth:(float)viewWidth andHeight:(float)viewHeight;
- (void) updateRenderingPrimitives;
方法finishOpenGLESCommands , freeOpenGLESResources分別對應(yīng)著結(jié)束OpenGL和釋放OpenGL的資源。configureVideoBackgroundWithViewWidth:andHeight: , updateRenderingPrimitives和類SampleAppRenderer公開的方法名一樣,這里我猜測它們作用是一樣的。setOffTargetTrackingMode:方法作用目前還不是很清晰,需要去.mm文件中詳查。
??現(xiàn)在進入實現(xiàn)文件中,源碼中提到了關(guān)于OpenGL線程安全的問題。iOS上的OpenGL ES是線程不安全的,在程序中Vuforia使用下面的方法來保證線程(OpenGL 上下文)安全:
- a、在主線程中創(chuàng)建OpenGL ES上下文。
- b、Vuforia相機開始時,將其位于我們自己EAGLView視圖上,并開啟renderer線程。
- c、Vuforia會在renderer線程上,定期調(diào)用我們的renderFrameVuforia(SampleAppRenderer類提到)方法。當?shù)谝淮握{(diào)用該方法的時候,
defaultFramebuffer并不存在,調(diào)用createFramebuffer方法來創(chuàng)建它。createFramebuffer由主線程調(diào)用,而與此同時renderer線程會被阻塞。因此確保OpenGL ES上下文不會被并行使用
在initWithFrame:appSession:的實現(xiàn)方法中會進行session,OpenGL的context和Renderer的賦值,初始化和綁定工作。而方法configureVideoBackgroundWithViewWidth:andHeight: , updateRenderingPrimitives在其實現(xiàn)方法中的確只是簡單的調(diào)用了一下SampleAppRenderer 類的實例方法。
??現(xiàn)在主要來看看方法setOffTargetTrackingMode :,它的實現(xiàn)很簡單只是對其私有成員變量NO。但是卻在協(xié)議方法renderFrameWithState中大量的使用。該方法大部分是OpenGL相關(guān)的工作,我沒有深究下去,只整理出來一個工作流程圖:

目前來看ImageTargetsEAGLView類的主要作用在于保證OpenGL在iOS中達到線程安全,創(chuàng)建buffer和對buffer的管理,提供了對OpenGL的控制,而實際的渲染則由SampleAppRenderer來實現(xiàn)。
ImageTargetsViewController類
現(xiàn)在將目光回到ImageTargetsViewController類上面來。由于是一個控制器類,所以我直接從.mm文件中著手。根據(jù)ViewController的加載順序來看具體的邏輯,首先查找loadView方法,如果沒有則查找viewDidLoad。源碼中,loadView方法主要創(chuàng)建了vapp,eaglView以及對vapp初始化了AR相關(guān)的事務(wù)(其他視圖和手勢等先忽略,只關(guān)心屬性vapp,eaglView相關(guān)的邏輯),將ViewController的View設(shè)置為eaglView。
??在loadView中將vapp的代理設(shè)置為控制器自身,此時通過上面介紹__ SampleApplicationSession__時對應(yīng)的程序執(zhí)行流程,將目光放在對應(yīng)的部分協(xié)議方法上面。
@protocol SampleApplicationControl
- (void) onInitARDone:(NSError *)error;
- (bool) doInitTrackers;
- (bool) doLoadTrackersData;
- (bool) doStartTrackers;
@end
從圖-1可以看出是由doInitTrackers的返回值來判斷是否需要去加載tracker數(shù)據(jù)(doLoadTrachersData),最后在onInitARDone方法流程結(jié)束。通過這個就確定了我們的源碼查看順序:
/// doInitTrackers --> doLoadTrackersData --> onInitARDone
那么在ImageTargetsViewController類中,其流程圖如下:

自此我們的源碼閱讀就告一段落,最后我們將要去實現(xiàn)開始提到的目的!
收尾
讀到這里,自定義的數(shù)據(jù)集的切入點在方法doLoadTrackersData方法中,并且要doInitTrackers方法返回YES。如果沒有執(zhí)行“安裝步驟”中的第5步的話,現(xiàn)在可以去做了!做完之后添加如下代碼到工程中:
///ImageTargetsViewController.mm -> doLoadTrackersData
dataSetCustom = [self loadObjectTrackerDataSet:@"WillDB_Device.xml"];/// 這個dataset為你自己的名字
if (dataSetCustom == NULL) {
return NO;
}
if (! [self activateDataSet:dataSetCustom]) {
return NO;
}
運行程序,掃描對應(yīng)的圖片發(fā)現(xiàn)是能夠成功掃描對應(yīng)的圖片的。但是系統(tǒng)的圖片能出來一個“茶壺”,而我們自己的圖片上面什么也沒有呢?
在這里我不想又去使用這個煩人的“茶壺”O(jiān)penGL模型了,我選擇的是一個皮卡丘的原型(在文末我會將改造過的demo傳到Github可以去那里下載這個原型)。
模型obj到opengl數(shù)據(jù)的轉(zhuǎn)換
就目前我知道的來說,在Xcode中無法使用.obj的模型數(shù)據(jù)的。我在網(wǎng)上找到了一個工具obj2opengl,具體的使用方法見這里,我還是大體來說一下使用步驟:
將下載好的文件放到特定的文件夾中,然后把對應(yīng)的obj文件和它放在一起,使用終端進入obj2opengl.pl文件所在文件夾之后,輸入如下命令:
./obj2opengl.pl yourobjfilename
成功后,它會生成一個頭文件,這就是通過obj文件生成的紋理坐標代碼,在該頭文件中有3個數(shù)組,這三個數(shù)組分別對應(yīng)著xxxVerts [], xxxNormals [], xxxTexCoords [],和一個xxxxNumVerts(xxx為你的obj文件名字),具體使用說明。
模型替換
通過前面的源碼閱讀,我們知道ImageTargetsViewController類是用來加載tracker數(shù)據(jù)的,SampleAppRenderer類是做渲染相關(guān)的數(shù)據(jù),SampleApplicationSession類是使用tracker數(shù)據(jù)并控制AR,最后只剩下一個ImageTargetsEAGLView類。在該類中會做如下操作:
1、在textureFilenames數(shù)組中,添加一個新的紋理。這個自己選擇一個紋理圖片,我是隨便選的,所以看起來會很丑。
-
2、在ImageTargetsEAGLView類的頭文件中添加一個私有成員變量pikachuModel。用它來代替例子中的buildingModel。并在SampleApplication3DModel.h文件中添加方法
pikachu_ReWrite,并在SampleApplication3DModel.m文件中添加如下代碼:- (void)pikachu_ReWrite{ #if kUse3DModel == 1 _numVertices = XY_PikachuMNumVerts; _vertices = XY_PikachuMVerts; _normals = XY_PikachuMNormals; _texCoords = XY_PikachuMTexCoords; #endif } -
3、在ImageTargetsEAGLView.mm的方法
loadBuildingsModel中添加如下代碼:pikachuModel = [[SampleApplication3DModel alloc] init]; [pikachuModel pikachu_ReWrite]; 4、將ImageTargetsEAGLView.mm文件中所有的buildingModel替換為pikachuModel,最后調(diào)節(jié)一下變量kObjectScaleOffTargetTracking的值,這個值調(diào)節(jié)由自己決定。
上述的修改靈感大多是來自頭文件Teapot.h,但是我們使用obj2opengl時生成的文件中并沒有Teapot.h中teapotIndices對應(yīng)的數(shù)組。相反多了一個無符號的整形變量xxxNumVerts,所以除了上訴的方法以外還有另外一種方法,具體的代碼修改如下:
/// 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生成的頭文件的數(shù)組中的)數(shù)值比例是需要修改的,而且還需要對模型進行翻轉(zhuǎn)。這個方法具體見How do I replace the Teapot和Replace the teapot model。
在修改源碼時主要就是在修改方法renderFrameWithState,它在介紹ImageTargetsEAGLView類時在文件的最開頭就有提到,它是在每捕捉到一次tracker之后就會運行一次。
到這里一個很基本的Vuforia集成,源碼的解讀以及自定義tracker和模型就算完成了,最后附上Demo地址。


