實踐解析:利用ARKit實現直播場景虛擬化(上篇)

聲網Agora發表於2018-01-16

編者按:AR遊戲很常見,但在直播畫面中出現AR螢幕,在其中再巢狀直播畫面的玩法你試過麼?我們將通過兩篇文章由淺入深,帶你一起看看AR能與音視訊直播碰撞出怎樣的火花。

本篇我們將從ARKit開發原理,到demo程式碼,帶大家一起實現一個AR應用。

下篇我們將圍繞音視訊資訊採集,到場景渲染,與大家分享如何實現AR視訊會議的demo例項。歡迎訪問 RTC 開發者社群,與更多開發者交流經驗,參與更多技術活動。

今年7月Apple推出了AR工具ARKit,著實閃著了大家的眼睛。從目前的評測可以知道 ARKit已經非常成熟,完全可以進行商用了。

在iOS中,擴增實境由ARKit和渲染兩部分組成。ARKit主要負責AR計算,它將ARCamera捕獲的視訊幀當作背景,使用視覺慣性測距(VIO)來精確跟蹤周圍的世界,進行座標轉換,場景搭建及平面的捕獲;然後,通過 SceneKit(3D)/SpritKit(2D) 或 Metal 庫進行渲染,使虛擬物體與真實世界相接合,達到擴增實境的目的。

今天我們就來詳細的瞭解一下 ARKit,看看 Apple 為我們提供了怎樣強大的工具,可以讓我們迅速的構建一個AR應用程式。

在講解我們的 AR 程式之前,我們先要了解幾個ARKit的基本概念。只有這幾個基本概念瞭解清楚之後,我們才能清楚的知道如何去寫一個AR程式。

實踐解析:利用ARKit實現直播場景虛擬化(上篇)

幾個重要概念

  • 空間定位與方向追蹤 這個是通過 ARCamera 獲取到視訊幀,再經過 VIO 計算出來的。
  • 場景理解,平臺檢測 ,點選檢測,光照檢測 這是通過ARSession管理的內部模組計算得出的。
  • 渲染層 可以通過 SceneKit/SpritKit 或 Metal/OpenGL 進行渲染。今天主要介紹 SceneKit進行渲染。

什麼是特徵點

AR 的目標是往真實世界中的特定點插入虛擬內容,並且在真實世界中移動時還能對此虛擬內容保持追蹤。

ARKit 從視訊幀中獲得某張圖片的特徵後,就可以從多個幀中追蹤這些特徵。隨著使用者在真實世界中的移動,就可以利用相應的特徵點來估算 3D 姿態資訊。使用者移動地越多,就會獲得越多的特徵,並優化這些估算的 3D 姿態資訊。

有沒有可能檢測不出特徵點的情況呢?當然有,可能檢測不出特徵點的情況如下:

  • 光線差 沒有足夠的光或光線過強的鏡面反光。嘗試避免這些光線差的環境。
  • 缺少紋理 如果攝像頭指向一面白牆,那也沒法獲得特徵,ARKit 也去無法找到並追蹤使用者。嘗試避免看向純色、反光表面等地方。
  • 快速移動 通常情況下檢測和估算 3D 姿態只會藉助圖片,如果攝像頭移動太快圖片就會糊,從而導致追蹤失敗。但 ARKit 會利用視覺慣性里程計,綜合圖片資訊和裝置運動感測器來估計使用者轉向的位置。因此 ARKit 在追蹤方面非常強大。

什麼是平面檢測

ARKit 的平面檢測用於檢測出現實世界的水平面,也就是在 3D 空間中,Y值為0的一個區域。平面檢測是一個動態的過程,當攝像機不斷移動時,檢測到的平面也會不斷的變化。此外,隨著平面的動態檢測,不同平面也可能會合併為一個新的平面。

只有檢測真實世界有水平面之後,才能找到錨定點,並將虛擬物體放到這個錨定點上。

什麼是點選檢測

除了平臺檢測外,還有點選檢測。顧名思意,就是當使用者點選螢幕時,ARKit 將點選螢幕的2D空間位置轉換為ARKit 通過 ARCamera 捕獲到的視訊幀的 3D 空間位置。並在這個位置檢測是否有平面。

什麼是世界追蹤

世界追蹤都追蹤什麼呢?ARKit 會追蹤以下幾個資訊:

  • 追蹤裝置的位置以及旋轉,這兩個資訊均是相對於裝置起始時的資訊。
  • 追蹤物理距離(以“米”為單位),例如 ARKit 檢測到一個平面,我們希望知道這個平面有多大。
  • 追蹤我們手動新增的希望追蹤的點,例如我們手動新增的一個虛擬物體

ARKit 使用視覺慣性測距技術,對攝像頭採集到的影象序列進行計算機視覺分析,並且與裝置的運動感測器資訊相結合。ARKit 會識別出每一幀影象中的特徵點,並且根據特徵點在連續的影象幀之間的位置變化,然後與運動感測器提供的資訊進行比較,最終得到高精度的裝置位置和偏轉資訊。

除了上面這幾個概念外,我們還需要知道ARKit提供的一些基本知識。

ARSession

ARSession 是 ARkit 的核心。它是連線ARCamera與ARACNView之間的橋樑。像捕獲視訊,與 CoreMotion 的資料整合,場景的理解,平面檢測等等都需要 ARSession 來協調各模組進行協同處理。

另外,ARSession 有兩種獲取 ARFrame 的方法:

  • push 實時不斷的獲取相機位置,由ARSession主動告知使用者。通過實現ARSession的代理來獲取。

    (void)session:(ARSession *)session didUpdateFrame:(ARFrame *)frame
    複製程式碼
  • pull 使用者想要時,主動去獲取。通過 ARSession的屬性currentFrame來獲取。

ARConfiguration

該類的作用是設定一些 ARSession 相關的配置,如是否使用平面檢測就是通過這個引數來設定的。

ARSCNView

ARSCNView 繼承自 SceneKit 中的 SCNView。ARSCNView是一個非常複雜的類,它不僅擁有SCNView的功能,而且它還管理著 ARSession。如下圖所示:

實踐解析:利用ARKit實現直播場景虛擬化(上篇)

SceneKit 的主要作用是將虛擬物體展示在3D場景中。每個虛擬物體都可以用 SCNNode 來代表,SCNNode 在 SCNScene 中展現,而無數SCNScene 組成 3D 世界。

它有幾個重要的方法需要特別強調一下:

  • hitTest 方法

    - (NSArray<ARHitTestResult *> *)hitTest:(CGPoint)point types:(ARHitTestResultType)types;
    複製程式碼

    point:2D座標點(手機螢幕某一點);

    ARHitTestResultType:捕捉型別是 點 還是 面;

    NSArray<ARHitTestResult *> *:追蹤結果陣列。陣列的結果排序是由近到遠。

    根據2D座標點搜尋3D模型位置。當我們在手機螢幕點選某一個點的時候,可以捕捉到這一個點所在的3D模型的位置。為什麼返回值是一個陣列呢?這是因為手機螢幕一個是長方形的二維空間,而相機捕捉到的是一個由這個二維空間對映出去的長方體。我們點選螢幕一個點,可以理解為在這個長方體的邊緣射出一條線,這一條線上可能會有多個3D物體模型。

  • renderer 方法

    (void)renderer:(id<SCNSceneRenderer>)renderer updateAtTime:(NSTimeInterval)time
    複製程式碼

    它是 ARSCNViewDelegate 中的回撥方法,每次 3D 引擎要渲染新的視訊幀時都會呼叫該方法。

  • 過載 renderer 方法

    - (void)renderer:(id<SCNSceneRenderer>)renderer didAddNode:(SCNNode *)node forAnchor:(ARAnchor *)anchor
    複製程式碼

    它是 ARSCNViewDelegate 中的回撥方法,每次 ARKit 檢測到了平面時都會呼叫此方法。我們可以通過這個代理方法得知我們新增一個虛擬物體到AR場景下的錨點(AR現實世界中的座標)

SCNNode

SCNNode代表一個虛擬物體。通過 SCNNode 可以對虛擬物體進行變換和旋轉,還可以做幾何變換,光照等操作。

SCNScene

在ARKit中它代表一個場景。SCNScene 包括背景 和 虛似物體。其中背景可以是從 ARCamera捕獲的視訊幀。而虛擬物體由 rootNode 儲存,它就是前面介紹的 SCNNode。

ARAnchor

包含真實世界位置和方向的資訊。通過它可以輕鬆地將虛擬物體新增,更新或從會話中刪除。

ARCamera

ARCamera 用於捕捉視訊流。一般我們無需去建立一個ARCamera,因為在初始化 AR 時,它就幫我們將ARCamera建立好了。另外,我們一般也不直接使用 ARCamera 的 API,預設都是設定好的。

ARFrame

攝像頭視訊幀的包裝類。從 ARCamera 中獲取的每一幅視訊幀都被封裝成 ARFrame。它包含位置追蹤資訊、環境引數、視訊幀。重點是它包含了蘋果檢測的特徵點,通過rawFeaturePoints可以獲取,不過只是特徵的位置,具體的特徵向量並沒有開放。

SCNMaterial

使用 SCNMaterial 可以對虛擬物體 SCNNode 進行貼圖。

AR 任意門的實現

所謂任意門就是在真實環境中虛擬一扇門,當走進這扇門後,可以看到另外一個世界。

實現

初始化ARKit

- (void)viewDidLoad {
    [super viewDidLoad];

    // Set the view's delegate
    self.sceneView.delegate = self;
    
    // Show statistics such as fps and timing information
    self.sceneView.showsStatistics = YES;
    
    // Create a new scene
    SCNScene *scene = [SCNScene new];
    
    // Set the scene to the view
    self.sceneView.scene = scene;
    
    //Grid to identify plane detected by ARKit
    _gridMaterial = [SCNMaterial material];
    _gridMaterial.diffuse.contents = [UIImage imageNamed:@"art.scnassets/grid.png"];
    //when plane scaling large, we wanna grid cover it over and over
    _gridMaterial.diffuse.wrapS = SCNWrapModeRepeat;
    _gridMaterial.diffuse.wrapT = SCNWrapModeRepeat;
    
    _planes = [NSMutableDictionary dictionary];
    
    //tap gesture
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(placeTransDimenRoom:)];
    [self.sceneView addGestureRecognizer:tap];
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    
    // Create a session configuration
    ARWorldTrackingConfiguration *configuration = [ARWorldTrackingConfiguration new];
    configuration.planeDetection = ARPlaneDetectionHorizontal;
    // Run the view's session
    [self.sceneView.session runWithConfiguration:configuration];
    
}

複製程式碼

處理ARKit檢測到的平面

用於提示可以用於互動的平面,後期模擬物理世界也要用到。

- (void)renderer:(id<SCNSceneRenderer>)renderer didAddNode:(SCNNode *)node forAnchor:(ARAnchor *)anchor{
    if ([anchor isKindOfClass:[ARPlaneAnchor class]] && !_stopDetectPlanes){
        NSLog(@"detected plane");
        [self addPlanesWithAnchor:(ARPlaneAnchor*)anchor forNode:node];
        [self postInfomation:@"touch ground to place room"];
    }
}
- (void)renderer:(id<SCNSceneRenderer>)renderer didUpdateNode:(SCNNode *)node forAnchor:(ARAnchor *)anchor{
    if ([anchor isKindOfClass:[ARPlaneAnchor class]]){
        NSLog(@"updated plane");
        [self updatePlanesForAnchor:(ARPlaneAnchor*)anchor];
    }
}
- (void)renderer:(id<SCNSceneRenderer>)renderer didRemoveNode:(SCNNode *)node forAnchor:(ARAnchor *)anchor{
    if ([anchor isKindOfClass:[ARPlaneAnchor class]]){
        NSLog(@"removed plane");
        [self removePlaneForAnchor:(ARPlaneAnchor*)anchor];
    }
}
複製程式碼

放置transDimenRoom

對於隱藏空間,抽象成兩個類來表達:transDimenRoom,transDimenStruct。

後者用於提供一些平板等基礎結構,前者將這些結構拼成一個房間,留一個門框出來讓使用者能夠看見裡面。

當需要放置任意門時,就用+transDimenRoomAtPosition:方法建立一個transDimenRoom,當使用者走進去時,用 -hideWalls: 隱藏四周的牆壁,切換成全景背景。

@interface transDimenRoom : SCNNode
@property (nonatomic, strong) SCNNode *walls;

+(instancetype)transDimenRoomAtPosition:(SCNVector3)position;
//TODO:check  if user in room
-(BOOL)checkIfInRoom:(SCNVector3)position;

-(void)hideWalls:(BOOL)hidden;
@end

複製程式碼

檢測到使用者走進房間

目前為了簡單起見,是判斷使用者與房間中心的距離,當距離小於1時,就認為使用者進入了房間。這裡的邏輯以後會收歸到transDimenRoom中。

- (void)renderer:(id<SCNSceneRenderer>)renderer updateAtTime:(NSTimeInterval)time{
    if (_room.presentationNode) {
        
        SCNVector3 position = self.sceneView.pointOfView.presentationNode.worldPosition;
        
        SCNVector3 roomCenter = _room.walls.worldPosition;
        
        CGFloat distance = GLKVector3Length(GLKVector3Make(position.x - roomCenter.x, 0, position.z - roomCenter.z));
        
        if (distance < 1){
            NSLog(@"In room");
            [self handleUserInRoom:YES];
            return;
        }
        
        [self handleUserInRoom:NO];
        
    }
}
複製程式碼

小結

今天首先向大家介紹了一下 ARKit 的基本知識,然後通過任意門這個例項告訴了大家如何寫一個ARKit程式。這個 任意門 可以應用在很多場景中,大家可以通過這個例項進行擴充套件,充份發揮自己的想像力。

其實本節最最關鍵的是讓大家知道 ARKit中的那些基本概念。ARSession是它的核心,它協調內部模組進行場景的各種計算。而 ARSCNView 只是渲染技術中的一種,我們完成可以通過 OpenGL/Metal 來替換掉它。

在下篇,我們將會介紹,如何講ARkit應用於視訊直播。


相關閱讀:AR實踐:基於ARKit實現電影中的全息視訊會議

相關文章