本文与注释版代码地址中的README.md文件搭配阅读,效果更佳.html
ARKit系列文章目录node
官方代码地址react
2017年的苹果发布会,苹果演示过ARKit的一个Demo,名为InteractiveContentwithARKit
,对,就是那只变色龙!!ios
主要演示了下面的问题: 本示例演示了如下概念:c++
因为整个项目很是简单,只有几个主要文件: git
其中Extensions.swift
只是一个简单的工具类. ViewController.swift
中也只有几个点击事件及渲染循环. 具体各个函数的做用及调用时机在README.md文件中也有说明.github
咱们要关注的是Chameleon.swift
中几个有趣的方法实现.swift
动画的播放很是简单,找到节点,添加动画就能够了:闭包
// anim为SCNAnimation动画
contentRootNode.childNodes[0].addAnimation(anim, forKey: anim.keyPath)
复制代码
那么动画怎么来的?它是根据名称从.dae文件中加载的.而.dae文件是个场景文件,即SCNScene,对它的rootNode进行遍历,根据animationKey
找到对应的animationPlayer
就能够了.app
static func fromFile(named name: String, inDirectory: String ) -> SCNAnimation? {
let animScene = SCNScene(named: name, inDirectory: inDirectory)
var animation: SCNAnimation?
// 遍历子节点
animScene?.rootNode.enumerateChildNodes({ (child, stop) in
if !child.animationKeys.isEmpty {
// 根据key找到对应的player
let player = child.animationPlayer(forKey: child.animationKeys[0])
animation = player?.animation
stop.initialize(to: true)
}
})
animation?.keyPath = name
return animation
}
复制代码
这样就完了么??没有那么简单的,在转身的动画中,SCNAnimation只是让变色龙有了转身的动做,但节点并无真正转过来,因此在playTurnAnimation(_ animation: SCNAnimation)
中还使用了SCNTransaction
来让这个节点的transform
真正改变过来.
这个方法中求头部和摄像机之间夹角的方法挺有意思:
// 将摄像机视点的坐标,从世界坐标系转换到`head`的坐标系中
let cameraPosLocal = head.simdConvertPosition(pointOfViewPosition, from: nil)
// 摄像机视点坐标在`head`所在平面的投影(y值等于`head`的y值)
let cameraPosLocalComponentX = simd_float3(cameraPosLocal.x, head.position.y, cameraPosLocal.z)
let dist = simd_length(cameraPosLocal - head.simdPosition)
// 反三角函数求夹角,并转化为角度制
let xAngle = acos(simd_dot(simd_normalize(head!.simdPosition), simd_normalize(cameraPosLocalComponentX))) * 180 / Float.pi
let yAngle = asin(cameraPosLocal.y / dist) * 180 / Float.pi
let selfToUserDistance = simd_length(pointOfViewPosition - jaw.simdWorldPosition)
// 而后再根据夹角和距离,在其余函数中肯定须要播放的动画
复制代码
本来是个很简单的CAKeyframe旋转动画,将嘴巴张开.
// 绕x轴旋转
let animation = CAKeyframeAnimation(keyPath: "eulerAngles.x")
animation.duration = 4.0
animation.keyTimes = [0.0, 0.05, 0.75, 1.0]
animation.values = [0, -0.4, -0.4, 0]
animation.timingFunctions = [
CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut),
CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear),
CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
]
// 这是什么东西?是根据动画行进到不一样阶段触发的闭包回调
animation.animationEvents = [startShootEvent, endShootEvent, mouthClosedEvent]
mouthAnimationState = .mouthMoving
// 添加后即播放动画
jaw.addAnimation(animation, forKey: "open close mouth")
复制代码
可是须要在张开后触发发射舌头的动画,因此添加了animationEvents
以在keyTime不一样阶段触发不一样的回调
let startShootEvent = SCNAnimationEvent(keyTime: 0.07) { (_, _, _) in
self.mouthAnimationState = .shootingTongue
}
let endShootEvent = SCNAnimationEvent(keyTime: 0.65) { (_, _, _) in
self.mouthAnimationState = .pullingBackTongue
}
let mouthClosedEvent = SCNAnimationEvent(keyTime: 0.99) { (_, _, _) in
self.mouthAnimationState = .mouthClosed
self.readyToShootCounter = -100
}
复制代码
当self.mouthAnimationState
被改成.shootingTongue
后, reactToDidApplyConstraints(in sceneView: ARSCNView)
方法在每帧都会被调用,再调用了updateTongue(forTarget target: simd_float3)
判断出状态后,则开始移动舌头节点tongueTip
(缩回舌头.pullingBackTongue
也是一样):
currentTonguePosition = startPos + intermediatePos
// 将舌尖须要到达的位置`currentTonguePosition`从世界坐标系转换到舌尖父节点的动画位置处,并将转换处的位置赋值给`tongueTip`
tongueTip.simdPosition = tongueTip.parent!.presentation.simdConvertPosition(currentTonguePosition, from: nil)
复制代码
眼睛添加了SCNLookAtConstraint
约束,为了防止欧拉角引发死锁,因此要打开万向节锁
同时还添加了SCNTransformConstraint
约束,将x轴欧拉角限制在-20~+20
度内,左眼y轴欧拉角限制在5~150
度,右眼-5~-150
度
// 设置眼睛运动的约束
let leftEyeLookAtConstraint = SCNLookAtConstraint(target: focusOfLeftEye)
leftEyeLookAtConstraint.isGimbalLockEnabled = true
let rightEyeLookAtConstraint = SCNLookAtConstraint(target: focusOfRightEye)
rightEyeLookAtConstraint.isGimbalLockEnabled = true
let eyeRotationConstraint = SCNTransformConstraint(inWorldSpace: false) { (node, transform) -> SCNMatrix4 in
var eulerX = node.presentation.eulerAngles.x
var eulerY = node.presentation.eulerAngles.y
if eulerX < self.rad(-20) { eulerX = self.rad(-20) }
if eulerX > self.rad(20) { eulerX = self.rad(20) }
if node.name == "Eye_R" {
if eulerY < self.rad(-150) { eulerY = self.rad(-150) }
if eulerY > self.rad(-5) { eulerY = self.rad(-5) }
} else {
if eulerY > self.rad(150) { eulerY = self.rad(150) }
if eulerY < self.rad(5) { eulerY = self.rad(5) }
}
let tempNode = SCNNode()
tempNode.transform = node.presentation.transform
tempNode.eulerAngles = SCNVector3(eulerX, eulerY, 0)
return tempNode.transform
}
leftEye?.constraints = [leftEyeLookAtConstraint, eyeRotationConstraint]
rightEye?.constraints = [rightEyeLookAtConstraint, eyeRotationConstraint]
复制代码
读取着色器String,而后经过shaderModifiers
加载进去,经过字典来指定类型,SCNShaderModifierEntryPoint
的类型有geometry
,surface
,lightingModel
,fragment
.此处咱们指定为surface
类型.
如何给着色器传参呢??直接使用KVC,简单粗暴,可是挺好用的...
skin.shaderModifiers = [SCNShaderModifierEntryPoint.surface: shader]
skin.setValue(Double(0), forKey: "blendFactor")
skin.setValue(NSValue(scnVector3: SCNVector3Zero), forKey: "skinColorFromEnvironment")
let sparseTexture = SCNMaterialProperty(contents: UIImage(named: "art.scnassets/textures/chameleon_DIFFUSE_BASE.png")!)
skin.setValue(sparseTexture, forKey: "sparseTexture")
复制代码
而updateCamouflage(sceneView: ARSCNView)
和activateCamouflage(_ activate: Bool)
中则是经过KVC修改shader的参数值来激活/更新假装色.
最后,咱们来简单看下Metal的shader.
#pragma arguments供外部传入的参数
float blendFactor;
texture2d sparseTexture;
float3 skinColorFromEnvironment;
#pragma body
// 纹理和采样器声明
// 采样器,归一化坐标: normalized,寻址模式:clamp_to_zero, 滤波模式:linear
constexpr sampler sparseSampler(coord::normalized, address::clamp_to_zero, filter::linear);
// 采样结果,获得外部纹理sparseTexture中采样出的颜色
float4 texelToMerge = sparseTexture.sample(sparseSampler, _surface.diffuseTexcoord);
// 混合后赋值回去(实际上刚启动时传入的blendFactor=0,即用自带纹理;后面启动假装后,外部传入的blendFactor=1,即便用外部传入的纹理了)
_surface.diffuse = mix(_surface.diffuse, texelToMerge, blendFactor);
float alpha = _surface.diffuse.a;
// 根据外部传入的环境颜色,改变_suface的漫反射层的rgb值.
_surface.diffuse.rgb += skinColorFromEnvironment * (1.0 - alpha);
_surface.diffuse.a = 1.0;
复制代码
寻址模式中的 clamp_to_zero
跟OpenGL中的clamp-to-boarder
相似, 当采样到边界以外的时候, 若是该纹理不包含alpha
份量的,其颜色值永远为(0.0, 0.0, 0.0, 1.0)
, 不然, 该颜色值为(0.0, 0.0, 0.0, 0.0)
. Metal的shader是基于c++11(Metal2已是c++ 14了),添加了一些本身的语法同时也作了一些限制.详细的语法能够参考Metal Shading Language Guide.
变色龙这个Demo使用的是环境光贴图,没有真正的光源也就没有真正的阴影产生,而是使用了一些小技巧来产生了"假阴影",作法是在四只脚下面放上一块浅灰纹理的平面,这样仿佛就有了阴影.这也就是所谓的将光照和阴影"烘焙"进纹理中.
// The chameleon uses an environment map, so disable built-in lighting
// 禁用内置光照
sceneView.automaticallyUpdatesLighting = false
复制代码
// Load the environment map
// 加载光照环境贴图
self.lightingEnvironment.contents = UIImage(named: "art.scnassets/environment_blur.exr")!
复制代码
在苹果官方WWDC17中讲到,还能够用另外一种方法来产生实时的,真实的阴影.
write to color
中选项,这样平面就不会写入颜色缓冲中去,但阴影也会同时消失.
Deferred
,阴影就从新产生了.