[ARKit]3-蘋果官方AR變色龍Demo解讀

說明

本文與註釋版代碼地址中的README.md文件搭配閱讀,效果更佳.html

ARKit系列文章目錄node

官方代碼地址react

2017年的蘋果發佈會,蘋果演示過ARKit的一個Demo,名爲InteractiveContentwithARKit,對,就是那隻變色龍!!ios

主要演示了下面的問題: 本示例演示瞭如下概念:c++

  • 如何放置一個有交互動畫的CG物體(一個變色龍),並與其產生交互.
  • 如何根據用戶的移動和接近來觸發並控制物體的動畫.
  • 如何使用着色器shader來調整虛擬物體的外觀.

因爲整個項目很是簡單,只有幾個主要文件: git

WX20180204-184402@2x.png

其中Extensions.swift只是一個簡單的工具類. ViewController.swift中也只有幾個點擊事件及渲染循環. 具體各個函數的做用及調用時機在README.md文件中也有說明.github

咱們要關注的是Chameleon.swift中幾個有趣的方法實現.swift

preloadAnimations()動畫加載與播放

動畫的播放很是簡單,找到節點,添加動畫就能夠了:閉包

// 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真正改變過來.

relativePositionToHead(pointOfViewPosition: simd_float3)求夾角

這個方法中求頭部和攝像機之間夾角的方法挺有意思:

// 將攝像機視點的座標,從世界座標系轉換到`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)

// 而後再根據夾角和距離,在其餘函數中肯定須要播放的動畫 
複製代碼

openCloseMouthAndShootTongue()動畫過程當中觸發其它事件

本來是個很簡單的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)
複製代碼

setupConstraints()中的約束與萬向節鎖

眼睛添加了SCNLookAtConstraint約束,爲了防止歐拉角引發死鎖,因此要打開萬向節鎖

4039616-97978e4b06dd8ac8.gif

同時還添加了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]
複製代碼

setupShader()着色器的使用

讀取着色器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

最後,咱們來簡單看下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中講到,還能夠用另外一種方法來產生實時的,真實的陰影.

  1. 在物體正方,放置一塊平面,用來顯示陰影
    WX20180301-093643@2x.png
  2. 選中平面,在材質檢查器中,取消write to color中選項,這樣平面就不會寫入顏色緩衝中去,但陰影也會同時消失.
    WX20180301-093834@2x.png
  3. 從新顯示陰影,須要更改燈光的配置,選中燈光節點,進入光照檢查器
    WX20180301-093857@2x.png
  4. 將模式改成Deferred,陰影就從新產生了.
    WX20180301-093927@2x.png
相關文章
相關標籤/搜索