ARKit

简介

增强现实技术(Augmented Reality,简称 AR),是一种实时地计算摄影机影像的位置及角度并加上相应图像、视频、3D模型的技术,这种技术的目标是在屏幕上把虚拟世界套在现实世界并进行互动。ARKit框架提供了二种AR技术,分别基于三3D场景的SceneKit,以及基于2D场景的SpriktKit

原理

相机捕捉现实世界图像,计算3D模型的位置(ARKit负责)
呈现3D世界(SceneKit负责)
一般实现步骤:

  • 多媒体捕捉现实图像: 摄像头
  • 三维建模: 3D立体模型
  • 传感器追踪: 最终现实世界动态物体的六轴变化(只支持A9芯片之后的机型,也就是6S之后的机型)
  • 坐标识别及转换: 3D模型显示不是单纯的frame坐标点,而是三维的矩阵坐标
三维坐标轴.png
SceneKit介绍
  • SCNView
    SCNView是一个集成自UIView的视图,有个成员变量是scenescene相当于VC,scene.rootNode相当于self.view在UIKit中添加子视图是使用view.addSubview:xxx,那么在SceneView里,添加子节点使用scene.rootNode .addChildNode: xxNode

  • SCNScene
    自带一个定义了坐标系的root node(根节点),可以向内部增加树状结构的内部节点node,例如 lights光源, cameras相机, geometry几何体, particle emitters粒子发射源.需要放到SCNView的实例中呈现,SCNView在OSX(macOS)中是NSView的子类,在iOS中是UIView的子类;

  • SCNNode
    每一个scene创建的时候自带一个根节点,所有的子节点添加到根节点的下面,添加到scene中的node,默认在(x:0, y:0, z:0),即相对于父节点的位置.要想调整节点在父节点的位置,应该调整local coordinates(本地坐标系),而不是调整world coordinates(世界坐标系).

  • SCNGeometry
    系统自带了很多基础的几何体,添加到一个nodegeometry属性里。可以呈现出对应的形状cone(圆锥体), torus(圆环体), capsule(胶囊体), tube(管道),pyramid(四棱锥), box(长方体), sphere(球体), cylinder(圆柱体),

    let pyramid = SCNPyramid(width: 1, height: 1, length: 1)
    pyramid.firstMaterial?.diffuse.contents = UIImage(named: "1.PNG")
    let pyramidNode = SCNNode()
    pyramidNode.geometry = pyramid
    sceneView.scene?.rootNode.addChildNode(pyramidNode)
    

    SCNText前面那一种不同,它是一种立体字体,可以设置大小颜色

     let textNode = SCNNode()
     textNode.geometry = SCNText(string: "谢谢", extrusionDepth: 3)
    

    SCNShape添加任意形状,使用到UIBezierPath来绘制

    let bezierPath = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 1, height: 1))
    let shap = SCNShape(path: bezierPath, extrusionDepth: 1)
    let ovalNode = SCNNode()
    ovalNode.geometry = shap
    sceneView.scene?.rootNode.addChildNode(ovalNode)
    
  • SceneKit导入DAE文件
    1.直接添加到Bundle即项目内,用

    NSURL *url = [[NSBundle mainBundle]URLForResource:@"yizi" withExtension:@"dae"];
     // 创建场景
      SCNScene *scene = [SCNScene sceneWithURL:url options:nil error:nil];
    

    2.使用.scnassets文件,添加文件,找到Asset Catalog后缀命名.scnassets

    SCNScene * scene = [SCNScene sceneNamed:@"XX.scnassets/yizi.DAE"];
    
  • SCNPhysicsBody
    1.static:不受外力碰撞的物体,不能移动。比如地板,墙壁。
    2.dynamic:可以受力和碰撞影响的物体。
    3.kinematic: 不受外力或者碰撞影响的物理实体,但是它会在移动时造成碰撞影响其他的物 体。
    4.dynamicBodykinematicBody的默认类别是SCNPhysicsCollisionCategoryDefault,staticBody的默认类别为SCNPhysicsCollisionCategoryStatic;dynamicBodykinematicBody的默认质量是1,staticBody的默认质量为0
    5.举例:在桌球游戏中,球桌是staticBody,球是dynamicBody,球杆是kinematicBody
    6.open var contactTestBitMask: Int:当物体A与物体B接触时,SceneKit会比较A.contactTestBitMask && B.categoryBitMask,如果结果是一个非0的值,会创建一个SCNPhysicsContact对象,我们可以通过contactDelegate来处理我们需要自定义的一些操作。
    iOS8或者OS X v10.10的环境下,当且仅当碰撞发生了,SceneKit才会触发delegate
    iOS9或者OS X v10.11以后的版本,这个值默认是0,且不管是碰撞,还是穿过彼此,都会触发delegate.
    7.上面的属性需要作用在SCNNodephysicsBody

    let floorNode = SCNNode()
    floorNode.physicsBody = SCNPhysicsBody.static()
    
  • SCNParticleSystem粒子系统
    1.创建好的粒子系统如何加载使用?

    let particleSystem = SCNParticleSystem(named: "fire.scnp", inDirectory: nil)
    let particleNode = SCNNode()
    particleNode.addParticleSystem(particleSystem!)
    
  • SCNPhysicsBehavior物理行为,这个类是物理行为的基类,一般使用它的子类.
    1.SCNPhysicsHingeJoint: 连接两个物体,并允许他们在一个单一的轴上围绕对方转动
    2.SCNPhysicsBallSocketJoint:连接两个物体,并允许他们在任何方向上围绕对方转动
    3.SCNPhysicsSliderJoint: 连接两个物体,并允许他们彼此之间滑动或旋转。滑块关节像电机一样工作,在两个物理身体之间施加力或转矩。
    4.SCNPhysicsVehicle: 组合物理身体成为类似汽车底板的东西,你可以控制汽车的驾驶,刹车和加速。使用SCNPhysicsVehicleWheel对象定义车轮的外观和物理属性。
    5.使用注意事项:
    axisA axisB 沿着哪个轴转动,比如(1,0,0)沿着X轴转动anchorA anchorB A和B的锚点。一般的物体锚点在中心位置,但是有些几何体的锚点不在几何体的中心,比如SCNText的这样几何体, 它的锚点在左下角(备注:图片锚点标注错误应为(0.5,1,0))

    let joint = SCNPhysicsHingeJoint(bodyA: boxNode.physicsBody!, axisA:SCNVector3(1, 0, 0) , anchorA: SCNVector3(0, -2, 0), bodyB: textNode.physicsBody!, axisB: SCNVector3(1, 0, 0), anchorB: SCNVector3(0.5, 1, 0))
    sceneView.scene?.physicsWorld.addBehavior(joint)
    
锚点.png
  • SCNAction: 和Core Animation无缝交互

    let action = SCNAction.move(to: SCNVector3(0, 4, 20), duration: 3)
    let removeFromSuper = SCNAction.removeFromParentNode()
    let customAction = SCNAction.sequence([action,removeFromSuper])
    textNode.runAction(customAction)
    
  • SCNConstraint,是一个抽象约束类,不能直接使用,可以使用它的子类

    1. SCNLookAtConstraint: 让一个节点的方向,总是指向另外一个节点。第一视角的游戏中,让摄像机随时捕捉任务移动时候的位置,需要给照相机添加一个SCNLookAtConstraint类型的束;原理是更改节点的transform的属性;初始化方法:
     public convenience init(target: SCNNode?)
    

    target指向的是目标节点
    2.SCNTransformConstraint:创建一个旋转约束(提供给节点一个新的转换的计算),当进行下一次渲染的时候,会重新计算这个约束来调整节点的状态;初始化方法

    public convenience init(inWorldSpace world: Bool, with block: @escaping (SCNNode, SCNMatrix4) -> SCNMatrix4)
    

    world设置为YES.使用世界坐标系,设置为NO,使用自身坐标系
    3.SCNIKConstraint: 反向运动约束,将一个节点链移动到一个目标位置 。使用步骤:
    > 创建一个节点链
    > 给根节点添加SCNIKConstraint约束对象(胳膊)
    > 添加约束给执行器(手)
    > 限定链式节点移动的范围
    > 设置目标位置,这个值可以动态的改变

  • GLSL: 一种着色器语言,我们可以自定义程序片段,它在GPU上执行,代替了固定的渲染管线的一部分,如视图转换、投影转换等。它由片段着色器和顶点着色器组成。SCNGeometry.shaderModifiers有四种key:geometry(顶点),fragment(片段),lightingMode(灯光),surface(表面)

     Shader modifiers can be used to tweak SceneKit rendering by adding custom code at the    following entry points:
       1. vertex
       2. surface
       3. lighting
       4. fragment
    

    把着色器加载进程序中:创建.shader文件
    "右键"-> "New Flie" -> 选择“other” -> "Empty"(创建.shader文件)
    写入:

    float dotProduct = max(0.0, dot(_surface.normal, _light.direction));
    _lightingContribution.diffuse = vec3(0.1,0.1,0.1);
    

    加载:

    //顶点
    let geometryUrl = Bundle.main.url(forResource: "mapGeometry", withExtension: "shader")
    let mapGeometry =  try! String(contentsOf: geometryUrl!, encoding: .utf8)
    //灯光 
    let mapLightUrl = Bundle.main.url(forResource: "mapLighting", withExtension: "shader")
    let mapLighting = try! String(contentsOf: mapLightUrl!, encoding: .utf8)
    mapNode.geometry?.shaderModifiers = [SCNShaderModifierEntryPoint.geometry:mapGeometry,SCNShaderModifierEntryPoint.lightingModel:mapLighting]
    
  • automaticallyAdjustsZRange:自动调整相机zFar的值(何为zFar参考文中图片)
    小于zNear和大于zFar的无法显示

    ZFar.png

例子
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 添加SCNView
        let scnView = SCNView(frame: view.bounds)
        scnView.backgroundColor = UIColor.black
        //是否允许用户可以拖动屏幕来切换角度 默认false
        scnView.allowsCameraControl = true
        scnView.scene = SCNScene()
        view.addSubview(scnView)
        
        // 往scene中添加子节点(所有的子视图都以节点的方式添加)
        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        // 自动调整相机zFar的值(何为zFar参考文中图片)
        cameraNode.camera?.automaticallyAdjustsZRange = true
        // 在三维中的坐标
        cameraNode.position = SCNVector3(0, 1000, 1000)
        // 默认相机视角是平行向前,此时绕X轴旋转可以看到低视角的节点,前三个参数是确定怎么旋转,绕哪个轴旋转,哪个轴的值为1
        cameraNode.rotation = SCNVector4(1, 0, 0, -Float(Double.pi / 4))
        scnView.scene?.rootNode.addChildNode(cameraNode)
        
        // 添加一个光源
        let lightNode = SCNNode()
        lightNode.light = SCNLight()
        lightNode.light?.type = .spot
        // 光源的几何形状,系统还有多种其他的基础几何体
        lightNode.geometry = SCNSphere(radius: 20)
        // 光源发射的内容 (本身是白色,发射的黄色的光)
        lightNode.geometry?.firstMaterial?.emission.contents = UIColor.yellow
        // 光源本身的材质内容 (本身是黄色)
//        lightNode.geometry?.firstMaterial?.diffuse.contents = UIColor.yellow
        lightNode.position = SCNVector3(0, 1000, 40)
        // 灯光左右移动
        let leftAction = SCNAction.move(to: SCNVector3(-100, 1000, 40), duration: 2)
        let rightAction = SCNAction.move(to: SCNVector3(100, 1000, 40), duration: 2)
        // 相当于UIKit中的组动画
        let sequence = SCNAction.sequence([leftAction,rightAction])
        lightNode.runAction(SCNAction.repeatForever(sequence))
        scnView.scene?.rootNode.addChildNode(lightNode)
        
        // 添加一个环境光
        let ambientNode = SCNNode()
        ambientNode.light = SCNLight()
        ambientNode.light?.type = .ambient
        scnView.scene?.rootNode.addChildNode(ambientNode)
        
        let floorNode = SCNNode()
        floorNode.geometry = SCNFloor()
        floorNode.geometry?.firstMaterial?.diffuse.contents = "floor.jpeg"
        scnView.scene?.rootNode.addChildNode(floorNode)
        
        let treeNode = SCNScene(named: "palm_tree.dae")?.rootNode
        treeNode?.rotation = SCNVector4(1, 0, 0, -Float(Double.pi / 2))
        scnView.scene?.rootNode.addChildNode(treeNode!)
        
        //当使用SCNLookAtConstraint时,Scene Kit不管被朝向的对象如何移动,旋转都会让相机对着他.
        let constaint = SCNLookAtConstraint(target: treeNode)
        lightNode.constraints = [constaint]
        
        // 是否投下阴影
        lightNode.light?.castsShadow = false
        //遮光布 透视下来的投影图像
        lightNode.light?.gobo?.contents = "mip.jpg"
        //值越大 遮光布的效果越明显,投下的图像越显
        lightNode.light?.gobo?.intensity = 0.5
        //阴影模式有三种:deferred(递延),forward:(向前),modulated:(调节)
        lightNode.light?.shadowMode = .modulated
        
    }
效果.gif

ARKit介绍

  • ARSCNView:助手类,帮助我们用SceneKit渲染的3D内容来增强实时摄像头视图
    1.在视图中渲染设备摄像头的实时视频流,并就其设置为3D场景的背景
    2.ARKit中的3D坐标系会匹配SceneKit的3D坐标系
    3.自动移动虚拟的SceneKit3D摄像头来匹配ARKit追踪到的3D位置
  • ARSession: 负责控制摄像头聚合所有来自设备的传感数据等等以构建无缝体验
  • ARWorldTrackingSessionConfiguration: 这个类会告诉ARSession,在真实世界中追踪用户时需要使用六个自由度,roll/pitch/yaw以及xyz轴上的变换
  • 结合SceneKit简单的绘制一个AR3D立体图形
    1.首先在info.plist里面打开相机权限
    2.简单代码:运行,就可以看到在摄像机的正前方出现了一个box
    class ViewController: UIViewController {
      private let sceneView = ARSCNView()
      override func viewWillAppear(_ animated: Bool) {
          super.viewWillAppear(animated)
          
          let config = ARWorldTrackingConfiguration()
          sceneView.session.run(config, options: .resetTracking)
      }
    
      override func viewDidLoad() {
          super.viewDidLoad()
          view.addSubview(sceneView)
          sceneView.frame = view.bounds
          let geometry = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0.0)
          let boxNode = SCNNode(geometry: geometry)
          boxNode.position = SCNVector3(0, 0, -0.5)
          sceneView.scene = SCNScene()
          sceneView.scene.rootNode.addChildNode(boxNode)
          sceneView.automaticallyUpdatesLighting = true
      }
      
      override func viewWillDisappear(_ animated: Bool) {
          super.viewWillDisappear(animated)
          sceneView.session.pause()
      }
    }
    
  • 平面检测与视觉效果
    检测几何体对于增强现实app来说非常重要,因为要让用户感觉在和真实世界交互,就必须要知道用户是否敲击了桌面,或者正在看向地板,或者与其他表面进行交互。
  • 计算机视觉概念
    ARKit的基本流程包括从ios设备摄像头中读取频帧,对每一帧的图片进行处理并且获得特征点。特征点有很多,但我们需要从图片中找出能在多个帧中都被追踪到的特征。特征可以是物体的某一个角,或者是有纹理的某一条边等。有很多种方式可以生成这些特征,但是目前我们只需要知道,每一张图片里会产生多个唯一标识的特征就足够了;获得某个图片的特征之后,就可以从多个帧中追踪特征,随着用户在世界中移动,就可以利用相应的特征点来估算3D姿态信息
    至于平面检测,就是在获得一定数量的3D特征点之后,尝试在这些点中安装一些平面,然后根据尺度方向和位置找出最匹配的那个。ARKit会不断分析3D特征点并在代码中报告找到的平面
  • 检测不出特征点的原因可能有
    1.光线差
    2.缺少纹理(纯色、反光表面等)
    3.快速移动(图片会糊,追踪失败)
  • 增加DEBUG的视觉效果,检测出来的特征都会在世界中标识出来
    scnView.debugOptions = [ARSCNDebugOptions.showWorldOrigin,ARSCNDebugOptions.showFeaturePoints]
    
  • 设置追踪水平面
     // 明确表示需要追踪水平面。设置后 scene 被检测到时就会调用 ARSCNViewDelegate 方法
     configuration.planeDetection = .horizontal
    
  • ARSCNViewDelegate
    /**
    有新的 node 被映射到给定的 anchor 时调用。
    @param renderer 将会用于渲染 scene 的 renderer。
    @param node 映射到 anchor 的 node。
    @param anchor 新添加的 anchor。
    */
    func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor)
    func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor)
    func renderer(_ renderer: SCNSceneRenderer, didRemove node: SCNNode, for anchor: ARAnchor)
    
    didAdd中,检测到平面时创建新的Node;在构造方法中创建平面并且相应调整其大小
    didUpdate中,实时更新每个Node的位置;如果发现PlaneNode比预想的更大或者更小,就会更新平面的范围extent
    didRemove中,移除多个平面中共同Node,合并成一个平面。
    注: 在SceneKit中,平面默认是垂直的,所以需要绕x轴旋转90度来匹配
  • 命中测试
    如果用户点击屏幕,就会执行一次hit test,即获取屏幕的2D坐标,并从摄像头原点处通过屏幕的2D坐标发射一道射线到场景中,如果射线与某一个平面(plane)相交,就会获得命中结果,然后利用射线和平面相交的3D坐标,在此3D位置放置内容
    @objc func handleTapFrom(recognizer: UITapGestureRecognizer) {
          // 获取屏幕空间坐标并传递给 ARSCNView 实例的 hitTest 方法
          let tapPoint = recognizer.location(in: sceneView)
          let result = sceneView.hitTest(tapPoint, types: .existingPlaneUsingExtent)
          
          // 如果射线与某个平面几何体相交,就会返回该平面,以离摄像头的距离升序排序
          // 如果命中多次,用距离最近的平面
          if let hitResult = result.first {
              insertGeometry(hitResult)
          }
      }
    
    有了ARHitTestResult就可以得到射线/平面相交点的世界坐标,并在改位置放置虚拟目标。
  • 停止平面检测
    只需要更新ARSession configurationplaneDetection属性并再run一遍session即可。默认 情况下,session会保留相同的坐标系以及所有的anchor
    // 获取当前的 session configuration
    if let configuration = sceneView.session.configuration as? ARWorldTrackingSessionConfiguration{
                //关闭平面检测和更新
              configuration.planeDetection = .init(rawValue: 0) // ARPlaneDetectionNone
                // 再次 run session 以应用改变
              sceneView.session.run(configuration)
          }
    
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容