简介
增强现实技术(Augmented Reality,简称 AR),是一种实时地计算摄影机影像的位置及角度并加上相应图像、视频、3D模型的技术,这种技术的目标是在屏幕上把虚拟世界套在现实世界并进行互动。ARKit
框架提供了二种AR技术,分别基于三3D场景的SceneKit
,以及基于2D场景的SpriktKit
。
原理
相机捕捉现实世界图像,计算3D模型的位置(ARKit负责)
呈现3D世界(SceneKit负责)
一般实现步骤:
- 多媒体捕捉现实图像: 摄像头
- 三维建模: 3D立体模型
- 传感器追踪: 最终现实世界动态物体的六轴变化(只支持A9芯片之后的机型,也就是6S之后的机型)
- 坐标识别及转换: 3D模型显示不是单纯的frame坐标点,而是三维的矩阵坐标
SceneKit介绍
SCNView
SCNView
是一个集成自UIView
的视图,有个成员变量是scene
,scene
相当于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
系统自带了很多基础的几何体,添加到一个node
的geometry
属性里。可以呈现出对应的形状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.dynamicBody
和kinematicBody
的默认类别是SCNPhysicsCollisionCategoryDefault
,staticBody
的默认类别为SCNPhysicsCollisionCategoryStatic
;dynamicBody
和kinematicBody
的默认质量是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.上面的属性需要作用在SCNNode
的physicsBody
上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)
-
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
,是一个抽象约束类,不能直接使用,可以使用它的子类-
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
把着色器加载进程序中:创建
.shade
r文件
"右键"-> "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的无法显示
例子
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
}
ARKit介绍
-
ARSCNView
:助手类,帮助我们用SceneKit
渲染的3D内容来增强实时摄像头视图
1.在视图中渲染设备摄像头的实时视频流,并就其设置为3D场景的背景
2.ARKit
中的3D坐标系会匹配SceneKit
的3D坐标系
3.自动移动虚拟的SceneKit
3D摄像头来匹配ARKit
追踪到的3D位置 -
ARSession
: 负责控制摄像头聚合所有来自设备的传感数据等等以构建无缝体验 -
ARWorldTrackingSessionConfiguration
: 这个类会告诉ARSession
,在真实世界中追踪用户时需要使用六个自由度,roll/pitch/yaw以及xyz轴上的变换 - 结合
SceneKit
简单的绘制一个AR3D立体图形
1.首先在info.plist
里面打开相机权限
2.简单代码:运行,就可以看到在摄像机的正前方出现了一个boxclass 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 configuration
的planeDetection
属性并再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) }