ARKit 1.5更新 识别标记图像并实现AVPlayer流媒体播放

AR系列的开篇。本系列打算由上而下的学习AR的各种实现方式和原理。会涉及到音视频、视频照片滤镜和OpenGLES相关的知识。

让我们来定义一个需求,AR识别固定标记,识别后根据识别标记的位置计算旋转、平移和缩放矩阵确定播放器视口位置和大小,支持播放网络视频。当然实现的方式有多种:

效果图
  • 通过第三方 Vuforia可以实现AR识别功能,官方demo介绍了本地视频播放的方法,但是不支持流媒体播放。流媒体播放视频可以用AVPlayer加载URL,并添加AVPlayerItemOutput 为AVPlayerItem的output。调用下面的API获取视频帧缓存。
/*!
@abstract检索适合在指定的项目时间显示的图像,并将图像标记为已获取。
@discussion
完成后,客户端负责在返回的CVPixelBuffer上调用CVBufferRelease。
通常,您将调用此方法来响应CVDisplayLink回调或CADisplayLink委托调用,并且hasNewPixelBufferForItemTime:也返回YES。
从copyPixelBufferForItemTime:itemTimeForDisplay:中检索的缓冲区引用本身可能为NULL。 为NULL时表明该CMTime没有像素缓冲区需要显示。
 */
- (nullable CVPixelBufferRef)copyPixelBufferForItemTime:(CMTime)itemTime itemTimeForDisplay:(nullable CMTime *)outItemTimeForDisplay CF_RETURNS_RETAINED;

Vuforia 类似的详细实现之后专门开篇讨论。

  • ARKit实现识别图片。这是iOS 11.3推出的新功能。具体API如下
/**
AR场景中要检测的图片
 @discussion 如果设置detectionImages,ARKit将尝试检测指定的图像。当检测到图像时,
 会回调 - (nullable SCNNode *)renderer:(id <SCNSceneRenderer>)renderer nodeForAnchor:(ARAnchor *)anchor方法。anchor  是ARImageAnchor。
 */
@property (nonatomic, copy, nullable, readwrite) NSSet<ARReferenceImage *> *detectionImages API_AVAILABLE(ios(11.3));

本篇主要讨论ARKit实现方案。

先看几个11.3 新特性

  • iOS 11.3 新特性之一 ARPlaneDetectionVertical 这是一个NS_OPTIONS位移枚举。用于检测场景中的垂直平面。

  • iOS 11.3 新特性之二: ARReferenceImage API_AVAILABLE(ios(11.3))
    ARReferenceImage 是识别图像的模型类,提供了三个创建方法。init和new都设置为不可用。

// CGImageRef生成ARReferenceImage识别模型,注意physicalWidth的单位是:米 (meters)
- (instancetype)initWithCGImage:(CGImageRef)image orientation:(CGImagePropertyOrientation)orientation physicalWidth:(CGFloat)physicalWidth NS_SWIFT_NAME(init(_:orientation:physicalWidth:));
// 从CVPixelBufferRef生成ARReferenceImage识别模型
- (instancetype)initWithPixelBuffer:(CVPixelBufferRef)pixelBuffer orientation:(CGImagePropertyOrientation)orientation physicalWidth:(CGFloat)physicalWidth NS_SWIFT_NAME(init(_:orientation:physicalWidth:));

// 从指定的图片包里加载一套识别图,返回一个ARReferenceImage识别模型 的NSSet。
+ (nullable NSSet<ARReferenceImage *> *)referenceImagesInGroupNamed:(NSString *)name bundle:(nullable NSBundle *)bundle;

/** Unavailable */ 
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
  • iOS 11.3 新特性之三: ARWorldTrackingConfiguration的新属性detectionImages 。把我们上面创建的ARReferenceImage识别模型组赋值给detectionImages。
@property (nonatomic, copy, nullable, readwrite) NSSet<ARReferenceImage *> *detectionImages API_AVAILABLE(ios(11.3));

- iOS 11.3 新特性之四: SCNMaterial对象的contents可以直接添加AVPlayer对象作为纹理源。(iOS 11.3之前可以添加SpriteKit scene add child SKVideoNode  (SKVideoNode may creat with AVPlayer))
@property(nonatomic, retain, nullable) id contents;

   SCNMaterial *materialVideoFrame = [SCNMaterial material];
   materialVideoFrame.diffuse.contents = self.playerManger.player;

根据上诉四条新功能,可以实现AR视频播放。下面上代码:

  • 配置 ARWorldTrackingConfiguration 添加识别图, 并runWithConfiguration。
- (void)resetTracking
{
    /**
     ARSessionRunOptions 也是一个NS_OPTION。
     ARSessionRunOptionResetTracking 每次调用重新配置识别。
     ARSessionRunOptionRemoveExistingAnchors 重新配置时删除之前存在的Anchors
     
     如果不配置options:默认保留现有的Anchors
     */
    [self.sceneView.session runWithConfiguration:self.arConfig options:ARSessionRunOptionResetTracking | ARSessionRunOptionRemoveExistingAnchors];
}
- (ARWorldTrackingConfiguration *)arConfig
{
    if (!_arConfig) {
        _arConfig = [[ARWorldTrackingConfiguration alloc] init];

        //iOS 11.3 新特性之一: ARPlaneDetectionVertical API_AVAILABLE(ios(11.3)) = (1 << 1)
        /** Plane detection determines vertical planes in the scene. */

        if (@available(iOS 11.3, *)) {
// 由于我们识别的是图像平面,这里不设置其他平面的识别。
//            _arConfig.planeDetection = ARPlaneDetectionHorizontal | ARPlaneDetectionVertical;

            NSString *resourcePath = [[NSBundle mainBundle] resourcePath];
            NSString *filePath = [resourcePath stringByAppendingPathComponent:@"L3DWY888_800x600@2x.jpg"];
            _deImage = [UIImage imageWithContentsOfFile:filePath];
            
            //iOS 11.3 新特性之二: ARReferenceImage API_AVAILABLE(ios(11.3))

            ARReferenceImage *referenceDetectedImg = [[ARReferenceImage alloc] initWithCGImage:_deImage.CGImage orientation:1 physicalWidth:0.1];
//  iOS 11.3 新特性之三: ARWorldTrackingConfiguration的新属性
            [_arConfig setDetectionImages:[NSSet setWithObject:referenceDetectedImg]];
        }
    }
    return _arConfig;
}
  • 识别成功返回锚点,添加node,矩阵变换。
#pragma mark - ARSCNViewDelegate
// 重写以根据anchor创建添加到当前session中的node。识别水平垂直平面或图像返回的锚点。
- (SCNNode *)renderer:(id<SCNSceneRenderer>)renderer nodeForAnchor:(ARAnchor *)anchor {
    // 当返回ARImageAnchor时,说明识别到图片了。
    if (![anchor isMemberOfClass:[ARImageAnchor class]]) {
        return [SCNNode node];
    }
// 这里配置我们播放视频相关的node。
// 自定义SCNGeometry(SCNGeometrySource, SCNGeometryElement) 设置视频node
    DKVideoPlane *videoGeometry = [DKVideoPlane planeWithType:DKVideoPlaneHorizontal width:0.1 length:0.07];
    videoGeometry.materials = @[self.materials[DKARPlayerMaterialTypeVideo]];

    _videoNode = [SCNNode nodeWithGeometry:videoGeometry];
    //worldTransform 和 transform 的区别:worldTransform相对于根节点的旋转平移缩放矩阵,transform和position同时设置,共同作用与node的变换。
    SCNMatrix4 transM = SCNMatrix4FromMat4(anchor.transform);
    _videoNode.worldTransform = transM;
}

- (NSMutableArray<SCNMaterial *> *)materials
{
    if (!_materials) {
        _materials = [NSMutableArray array];
      
        SCNMaterial *materialVideoFrame = [SCNMaterial material];
        /**
         SKScene *ss = [SKScene sceneWithSize:CGSizeMake(100, 100)];
         SKVideoNode *vn = [SKVideoNode videoNodeWithAVPlayer:self.playerManger.player];
         [ss addChild:vn];
         materialVideoFrame.diffuse.contents = ss;
         */
        
        // iOS 11.3 新特性之四: SCNMaterial对象的contents可以直接添加AVPlayer对象作为纹理源。
        materialVideoFrame.diffuse.contents = self.playerManger.player;
        [_materials insertObject:materialVideoFrame atIndex:DKARPlayerMaterialTypeVideo];
    }
    return _materials;
}

  • 加上简单的播放逻辑,点击播放暂停,快进快退seek
- (void)setupRecognizers {
    // Single tap will insert a new piece of geometry into the scene
    UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onPlayerVideo:)];
    tapGestureRecognizer.numberOfTapsRequired = 1;
    [self.sceneView addGestureRecognizer:tapGestureRecognizer];
    
    UIPanGestureRecognizer *panRecognizer = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(panDirection:)];
    [panRecognizer setMaximumNumberOfTouches:1];
    [panRecognizer setDelaysTouchesBegan:YES];
    [panRecognizer setDelaysTouchesEnded:YES];
    [panRecognizer setCancelsTouchesInView:YES];
    [self.sceneView addGestureRecognizer:panRecognizer];
}

// 添加手势控制播放暂停
- (void)onPlayerVideo:(UITapGestureRecognizer *)sender {
    
    if (sender.state == UIGestureRecognizerStateEnded) {
        
        if ([self.playerManger getStatus] == PLAYING) {
            [self pause];
        }else{
            [self play];
        }
    }
}
// 添加手势控制播放进度
- (void)panDirection:(UIPanGestureRecognizer *)pan {
    CGPoint veloctyPoint = [pan velocityInView:self.sceneView];
    
    // 判断是垂直移动还是水平移动
    switch (pan.state) {
        case UIGestureRecognizerStateBegan: { // 开始移动
            // 使用绝对值来判断移动的方向
            CGFloat x = fabs(veloctyPoint.x);
            CGFloat y = fabs(veloctyPoint.y);
            if (x > y) { // 水平移动
                self.panDirection = DKARPanDirectionHorizontalMoved;
                CMTime time       = self.playerManger.player.currentTime;
                self.sumTime      = time.value/time.timescale;
            }
            break;
        }
        case UIGestureRecognizerStateChanged: {
            switch (self.panDirection) {
                case DKARPanDirectionHorizontalMoved:{
                    [self horizontalMoved:veloctyPoint.x];
                    break;
                }
                default:
                    break;
            }
            break;
        }
        case UIGestureRecognizerStateEnded: {
            switch (self.panDirection) {
                case DKARPanDirectionHorizontalMoved:{
                    [self.playerManger seekTo:self.sumTime];
                    self.sumTime = 0;
                    break;
                }
                default:
                    break;
            }
            break;
        }
        default:
            break;
    }
}

  • 基本完成开篇提出的需求。

开发过程中需要注意的点:

  • 需要对纹理坐标系和顶点坐标系有清晰的理解。
  • 注意node的SCNGeometry绘制方向。

待完善的地方:

  • hitTest: 不能击中已添加的ARImageAnchor,识别平面添加的anchor可以正确击中。接下来打算自定义点击区域计算方法。

  • SCNText没有正确显示到scene中

有不明白的欢迎讨论。本篇demo

识别标记图:

// 要识别的图片name。也可以定义图片包。
#define TrackImage @"aaa.jpg"
识别标记图
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容