ARKit 看这一篇就够了

@[TOC](ARKit 编程指南)

**欢迎转载----转载请注明出处**

源码 Github 链接 欢迎Star
如果 Github 不能访问,示例程序也可在CSDN下载 点我下载项目

前置条件

在学习ARKit 前,应当有一定的矩阵知识基础方便快速上手和理解背后的逻辑。
如果你对 Metal 有过了解和学习,那么你可以轻松的使用ARKit
如果你想要了解并学习 Metal 请点击本人的另一篇文章 Metal 编程指南
如果没有渲染方面的知识也没有关系,希望你能回顾一下 iOS 中关于 Layer 的 transform 动画

坐标转换

当我们展示 ARKit 时,只能使用 ARSCNViewCAMetalLayerCAEAGLLayer

ARSCNView 继承自 SCNView 关于 SCNView 网上文章有很多 主要是用来加载 3D 模型的。也是在初学者入门时最适合使用的一种 View,自带了 ARSession 和 SCNScene 可以不用去额外处理相机流。关于 SCNView 下面有更详细的使用。
CAMetalLayer 定义在 QuartzCore.framework 中,继承于 CALayer。它管理 Metal Texture Pool,并且负责渲染 MTLTexture 到窗口。我们可以将 ARSession 的相机流以 Texture 形式贴在屏幕上。
CAEAGLLayer 同样继承自 CALayer,主要是在使用 openGLES 时使用,渲染的原理和使用 CAMetalLayer 一样。

在这三种View 中,他们的坐标系都和我们平时使用的不一样。
总的来说就是我们屏幕中的一个点比如(X=100,Y=100)在这些View中是不存在的,他们的坐标系为 (-1,1)
因此如果我们需要添加一个模型或添加一个点则应该把 屏幕坐标系转换为 它们对应的坐标系。

下图表示了如果从 UIKit 坐标转为 Metal 空间坐标

坐标转换.png

规范化的设备坐标使用右手坐标系X向右,Y向下,Z朝向自己并映射到视口中的位置。这些坐标与视口大小无关。 Z 值指向远离摄像机的位置(进入屏幕)。


// 我们可以使用一段代码来验证坐标转换
float uikitX = 375;
float uikitY = 667;
float ratioX = 1.0 / 375;
float ratioY = 1.0 / 667;
float resultX = (2.0 *  uikitX * ratioX) - 1.0;
float resultY = (2.0 * -uikitY * ratioY) + 1.0;

NSLog(@"x=%.2f y=%.2f",resultX, resultY);
// x=1.00 y=-1.00

3D模型

当我们了解了坐标系的变化后就可以尝试加载一些模型了。
平时看到的 AR 场景都很炫酷,那是因为模型做的好看,和我们写的代码关系不大~
首先在 SceneKit 中,系统为我们提供了很多的 SCNGeometry,如果你使用过 Unity、UE5、GritGene 等渲染引擎应该对 Geometry 不陌生,做为基础的几何图形,他们构型了一个个复杂的3维模型。
我们先尝试使用 SCNView 来加载一个 SCNBox注:SCNBox 继承自SCNGeometry,简单而言就是一个正方体
关于 SCNView 的介绍可以自行查看官方文档,SCNView 中由SCNScene做为展示,每个模型都是一个节点,将节点添加到 SCNScene 的 rootNode 中即可。

@interface DeWuViewController ()
@property (nonatomic,strong) SCNView *scene;
@end

@implementation DeWuViewController
// 初始化一个 SCNView 
- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"得物试衣间";
    self.view.backgroundColor = UIColor.whiteColor;
    
    self.scene = [[SCNView alloc] initWithFrame:
                  CGRectMake(0, 88, self.view.frame.size.width, 200)];
    self.scene.backgroundColor = UIColor.lightGrayColor;
    self.scene.allowsCameraControl = true;
    SCNScene *rootScene = [SCNScene scene];
    self.scene.scene = rootScene;
    [self.view addSubview:self.scene];
}
@end

创建完成后看到的界面应该如下图


001.png

我们继续创建一个正方体,加载到屏幕中。

- (void)addBoxGeometry
{
    SCNBox *box = [SCNBox new];
    SCNNode *boxNode = [SCNNode nodeWithGeometry:box];
    [self.scene.scene.rootNode addChildNode:boxNode];
}

此时我们看到应该如下图所示,正方体的高度默认为 scene 的高度。


002.png

现在已经创建了一个正方体,它可能看起来不像,但不管你信不信,它确实是一个正方体!
如果我们打开了 SCNView 的 allowsCameraControl 属性,滑动屏幕应该能看到正方体。
我们可以通过设置 scene.rootNode.position 来使其变的小一些,但是肯定不是最佳的方法。因此我们需要了解矩阵的作用。
如果你做过 3D游戏、Metal、GLES。经常会看到一个 4×4 大小的矩阵,有四列和四行。
SceneKit 中,4x4矩阵使用的是 SCNMatrix4

typedef struct SCNMatrix4 {
    float m11, m12, m13, m14;
    float m21, m22, m23, m24;
    float m31, m32, m33, m34;
    float m41, m42, m43, m44;
} SCNMatrix4;

使用矩阵,可以通过三种方式转换对象:
翻译:沿 x、y 和 z 轴移动对象。
旋转:围绕任意轴旋转对象。
缩放:沿任意轴更改对象大小。

除此之外,还应该了解 投影变换
投影变换将节点的坐标从相机坐标转换为归一化坐标。根据您使用的投影类型,您将获得不同的效果。 如果你想了解更多请阅读 Metal 编程指南

举个简单例子:
如果我们希望图形绕着 X 轴旋转 则公式为:

[1 0 0 0]
[0 cos(-X Angle) -sin(-X Angle) 0]
[0 sin(-X Angle) cos(-X Angle) 0]
[0 0 0 1]

同理 沿着 Y 或 Z 使用对应的公式即可。我们并不是在讲数学的知识,所以在 SceneKit 中,系统已经帮我们做好了方法的封装,直接调用 SCNMatrix4MakeRotation 即可获得旋转后的 4x4 矩阵。 (如果你想了解矩阵的应用也可以查看我的博客 Metal 编程指南
当我们知道原理后,为正方体设置一个角度即可。

- (void)addBoxGeometry
{
    SCNBox *box = [SCNBox new];
    SCNNode *boxNode = [SCNNode nodeWithGeometry:box];
    boxNode.transform = SCNMatrix4MakeRotation(M_PI/2, 1, 1, 1);
    [self.scene.scene.rootNode addChildNode:boxNode];
}

这时我们应该能看到一个正方体了,由于 SCNBox 默认为白色,如果你想设置它的颜色或为它做纹理贴图应该使用 SCNMaterial 关于 SCNMaterial 会在下面继续说明,如果你学习了 Metal 那么可以理解它是一个片元着色器
有兴趣的可以自行搜索 SCNMaterial 学习
我们为 正方体设置一张图片,在来看看效果。

- (void)addBoxGeometry
{
   SCNBox *box = [SCNBox new];
   SCNMaterial *material = box.materials.firstObject;
   UIImage *img = [UIImage imageNamed:@"bricks"];
   material.diffuse.contents = img;
   SCNNode *boxNode = [SCNNode nodeWithGeometry:box];
   boxNode.transform = SCNMatrix4MakeRotation(M_PI/2, 1, 1, 1);
   
   [self.scene.scene.rootNode addChildNode:boxNode];
}
003.png

004.png

现在,我们可以尝试去加载由专业建模师做好的 3D模型了。
关于 SCNScene 可直接加载的模型请查看 官方文档 SCNSceneSource

由于模型一般比较大,使用异步线程来进行加载

- (void)addAppleWatch
{
    dispatch_queue_t _loadQueue = dispatch_queue_create("load_assets", DISPATCH_QUEUE_SERIAL);
    dispatch_async(_loadQueue, ^{
        SCNScene *scene = [SCNScene sceneNamed:@"AppleWatch.usdz"];
        SCNNode *watchNode = scene.rootNode.childNodes[0];
        SCNNode *watchRoot = [SCNNode node];
        // position 代表模型加载的位置
        // position.x 控制左右
        // position.y 控制上下
        // position.z 控制前后
        watchRoot.position = SCNVector3Make(-0.5, -0.8, -0.5);
        watchRoot.scale = SCNVector3Make(0.2, 0.2, 0.2);
        [watchRoot addChildNode:watchNode];
        [self.scene.scene.rootNode addChildNode:watchRoot];
    });
}

完成加载后如下图


005.png

平面监测和绘制

本次演示的平面监测使用 ARSCNView 前文提到过,ARSCNView 本身自带了 ARSession 不用额外去创建,不过在示例代码中有 使用 Metal 渲染的代码。包括横竖屏切换时纹理的裁剪。源码 Github 链接 欢迎Star

plane001.jpeg

plane002.jpeg

本节主要是讲通过 ARSession 找到平面并在平面上绘制一个正方形。
对于 ARSCNView 的一些属性请查看文档
本节和后续章节都是通过 ARSession 的回调来获取 ARAnchor 信息,因此对于展示的 View 选择方面并不是很重要。

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.view.backgroundColor = UIColor.blackColor;
    self.sceneView = [[ARSCNView alloc] initWithFrame:[UIScreen mainScreen].bounds];
    self.sceneView.session.delegate = self;
    self.sceneView.scene = [SCNScene new];
    // 设置debug 当找到特征点时 在屏幕中显示出来
    self.sceneView.debugOptions = ARSCNDebugOptionShowFeaturePoints;
    [self.view addSubview:self.sceneView];
}

初始化 ARSCNView 后,我们在页面出现是运行 ARSession
ARSession 作为 ARKit 的核心组件,管理并配置和运行不同的增强现实技术。
就运行而言,可以配置 平面检测、图片识别、人脸检测、骨骼检测、射线检测等..... 因此如果可以的话,应该对它要有一定的学习。
回到平面识别,ARConfiguration 作为运行的基类,我们所有的检测都是由它的子类的配置
平面识别:ARPositionalTrackingConfiguration
图片识别:ARImageTrackingConfiguration
骨骼识别:ARBodyTrackingConfiguration
等...

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    // ARPositionalTrackingConfiguration 是标准的平面识别类  但是是在 iOS13 时才推出
    // 在 iOS11 有AR时 就有了 ARWorldTrackingConfiguration 使用 WorldTracking 也能检测到平面
    // 只是精度没有 ARPositionalTrackingConfiguration 高。 如果你只在 iOS13 以上运行可以考虑使用 PositionalTracking
    ARWorldTrackingConfiguration *config = [ARWorldTrackingConfiguration new];
    config.planeDetection = ARPlaneDetectionHorizontal;
    [self.sceneView.session runWithConfiguration:config];
}

我们在初始化 ARSCNView 时为 ARSession 设置了代理。ARSession 当检测到 Anchor 时会进行回调,下列为ARSession 的回调方法

- (void)session:(ARSession *)session didAddAnchors:(NSArray<__kindof ARAnchor*>*)anchors;
- (void)session:(ARSession *)session didUpdateAnchors:(NSArray<__kindof ARAnchor*>*)anchors;
- (void)session:(ARSession *)session didRemoveAnchors:(NSArray<__kindof ARAnchor*>*)anchors;

通过方法名也能知道 当有 Anchor 时,会先进行 didAddAnchors: 方法告诉我们检测到了平面,当我们在转动摄像头时随着检测面的不断扩大,平面也会变大,因此会在 didUpdateAnchors: 告知我们那个面变大或变小了。最后如果一个地方我们长时间不用摄像头对准或两个平面合并成一个了,那么这个平面会消失,ARSession 通过 didRemoveAnchors: 方法告知我们那个平面没有了。

/// 检测到了平面

- (void)session:(ARSession *)session didAddAnchors:(NSArray<__kindof ARAnchor *> *)anchors
{
    for (ARAnchor *anchor in anchors)
    {
        // 返回的是 ARAnchor 基类 因此需要额外判断一下是不是 平面 ARPlaneAnchor
        if ([anchor isKindOfClass:[ARPlaneAnchor class]])
        {
            ARPlaneAnchor *pAnchor = (ARPlaneAnchor*)anchor;
            // 此时已经获取到了平面 我们创建一个 SCNPlane 
            // SCNPlane 和 上节用到的 SCNBox 一样 都是继承自 SCNGeometry 
            // 对于平面而言 肯定是有大小的,因此在初始化时要设置平面的大小 这个大小在 ARPlaneAnchor 中已经有了
            SCNPlane *geometry;
            if (@available(iOS 16.0, *)) {
                // 在 iOS16 中 新增了 planeExtent 属性 表明了平面的宽高
                geometry = [SCNPlane planeWithWidth:pAnchor.planeExtent.width height:pAnchor.planeExtent.height];
            } else {
                // 在 iOS16 以前 只有 extent 属性 用来获取屏幕的大小 X 表示宽 Z表示高
                geometry = [SCNPlane planeWithWidth:pAnchor.extent.x height:pAnchor.extent.z];
            }
            // 为平面设置一个纹理 和上节对 正方体一样都是由 SCNMaterial 来设置的
            SCNMaterial *material = geometry.materials.firstObject;
            UIImage *img = [UIImage imageNamed:@"bricks"];
            // contents 可以是图片、颜色或通过URL 加载的任何能被转为 Texture 的材质
            material.diffuse.contents = img;
            SCNNode *planeNode = [SCNNode nodeWithGeometry:geometry];
            // 为平面设置一个名称 方便后续查找 和 UIView.tag 一样
            // 每个 ARAnchor 都有一个 identifier 用来标识唯一性
            planeNode.name = pAnchor.identifier.UUIDString;
            // ARAnchor 中的 transform 属性用来表示 这个 Anchor 所在的位置和姿态
            // 我们设置一个方法 取出它对应的 X Y Z 
            // 注意 这个位置 是相对于 世界坐标系而言的位置,
            // 因此我们设置的属性应该是 worldPosition 而不是 position
            SCNVector3 pos = ExtractTranslation(pAnchor.transform);
            planeNode.worldPosition = pos;

            // 你也可以先试着不用设置下面这一行 来看看效果
            // SCNPlane 默认是竖着的 因此 我们需要让它沿着 X 轴旋转一次 达到我们想要的效果
            planeNode.transform = SCNMatrix4MakeRotation(-M_PI / 2.0, 1, 0, 0);
            
            [self.sceneView.scene.rootNode addChildNode:planeNode];
            
            // 在检测到的平面上放一个 正方体 
            SCNBox *box = [SCNBox boxWithWidth:0.2 height:0.2 length:0.2 chamferRadius:0];
            SCNNode *boxNode = [SCNNode nodeWithGeometry:box];
            // 这里我们将 Y 加0.15 使正方体看着能更加贴近平面
            boxNode.worldPosition = SCNVector3Make(pos.x, pos.y+0.15, pos.z);
            boxNode.scale = SCNVector3Make(0.5, 0.5, 0.5);
            
            [self.sceneView.scene.rootNode addChildNode:boxNode];
        }
    }
}
// 获取 4x4 矩阵中的 pos
SCNVector3 ExtractTranslation(const simd_float4x4& t)
{
    return SCNVector3Make(t.columns[3][0], t.columns[3][1], t.columns[3][2]);
}

/// 平面更新

- (void)session:(ARSession *)session didUpdateAnchors:(NSArray<__kindof ARAnchor *> *)anchors
{
    for (ARAnchor *anchor in anchors)
    {
        if ([anchor isKindOfClass:[ARPlaneAnchor class]])
        {
            ARPlaneAnchor *pAnchor = (ARPlaneAnchor*)anchor;
            // 通过 平面的 identifier 找到 对应平面
            SCNPlane *plane = [self findPlaneWith:pAnchor.identifier.UUIDString];
            if (!plane)
            {
                return;
            }
           // 更新平面的 宽高
            if (@available(iOS 16.0, *)) {
                plane.width = pAnchor.planeExtent.width;
                plane.height = pAnchor.planeExtent.height;
            } else {
                plane.width = pAnchor.extent.x;
                plane.height = pAnchor.extent.z;
            }
        }
    }
}

- (SCNPlane*)findPlaneWith:(NSString*)uuid
{
    for (SCNNode *childNode in self.sceneView.scene.rootNode.childNodes) {
        if ([childNode.geometry isKindOfClass:[SCNPlane class]])
        {
            if ([childNode.name isEqualToString:uuid])
            {
                return (SCNPlane*)childNode.geometry;
            }
        }
    }
    return nil;
}

/// 平面移除

- (void)session:(ARSession *)session didRemoveAnchors:(NSArray<__kindof ARAnchor *> *)anchors
{
    for (ARAnchor *anchor in anchors)
    {
        if ([anchor isKindOfClass:[ARPlaneAnchor class]])
        {
            // 当我们收到平面被移除的通知时 应该将包含平面的节点找到并移除它
            // 因此创建一个方法 通过之前设置的name 来找到节点 并删除
            ARPlaneAnchor *pAnchor = (ARPlaneAnchor*)anchor;
            SCNNode *node = [self findNodeWith:pAnchor.identifier.UUIDString];
            if (!node)
                return;
            [node removeFromParentNode];
        }
    }
}

- (SCNNode*)findNodeWith:(NSString*)uuid
{
    for (SCNNode *childNode in self.sceneView.scene.rootNode.childNodes) {
        if ([childNode.geometry isKindOfClass:[SCNPlane class]])
        {
            if ([childNode.name isEqualToString:uuid])
            {
                return childNode;
            }
        }
    }
    return nil;
}

骨骼监测

骨骼监测的 ARConfiguration 为 ARBodyTrackingConfiguration
如果你想了解它的属性可以查看 ARKit 中的定义。
当我们使用 ARBodyTrackingConfiguration 来运行 ARSession 时当找到人体也会进行和平面监测一样的回调,此时 ARAnchor 为 ARBodyAnchor,特征点中包含的骨骼的所有数据,需要注意的是 ARBodyAnchor 中的数据都是3D数据,如果我们希望获取骨骼的2D数据 应该使用 ARSession 的 didUpdateFrame: 回调方法

当监测到骨骼时由 didAddAnchors 方法进行回调通知,注意回调中的 Anchor 类型为 ARBodyAnchor 代表一个整体,里面包含了头、手、脚、肩等91个特征点。

body01.png
body02.png
body03.png
在检测到人体后,ARBodyAnchor 不会在变,当我们移动摄像头切换到下一个人物时,
对于 ARBodyAnchor 而言,只是其中的骨骼位置发生了变化,Anchor 还是同一个。
因此需要我们去追踪  ARBodyAnchor 的 isTracked 来判断人体是否离开了摄像机。

AR穿戴

w001.png
w002.png

简单使用骨骼监测,获取到用户关键骨骼的信息,在通过矩阵获取姿态将模型贴在上面。

图片追踪

AR导航

使用 ARKit 的一些常见问题及解决方案

请参考 GitHub 中的代码

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

推荐阅读更多精彩内容

  • ARKit 是苹果 WWDC2017 中发布的用于开发iOS平台 AR 功能的框架。AR 全称 Augmented...
    为什么划船不靠桨阅读 747评论 0 0
  • 引言ARKit 为开发 iPhone 和 iPad 增强现实(AR)app 提供了一个前沿平台。本文为你介绍 AR...
    蚂蚁安然阅读 8,928评论 0 14
  • @[TOC] ARKit3.5 框架学习 (一)核心类简介(上) 1. ARKit框架简介 ARKit整合iOS...
    孔雨露阅读 1,246评论 1 5
  • 0. 前言 作为一名刚入门的 iOS 开发者,前阵子稍稍研究了一下最新发布的 ARKit,然后结合几个其他开源项目...
    滑滑鸡阅读 4,331评论 5 31
  • ARKit是苹果在WWDC2017推出的一个用于实现增强现实(AR)的框架;开发者能够使用它来快速的完成基本的AR...
    己庚辛壬癸阅读 2,580评论 2 2