本文與註釋版代碼地址中的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
,陰影就從新產生了.