Apple Engine

Apple, iPhone, iOS, その周辺のことについて

WWDC 2017 の SceneKit サンプル Fox 2 を調べる その25

GameController クラスのセットアップ関数を見てゆく。
今回はカメラ設定

 

Fox2 では固定カメラの他に、プレイヤーキャラクターである Max を追尾するカメラがある。

追尾するカメラは、カメラ視点が変えられるものと、変えられないものの2つある。

 

紹介順の変更

GameController.swift で書かれている内容が前後するが、setupCamera、setupCameraNode、setActiveCamera 関数を先に見てゆく。

 

setupCamera()

イニシャライズに呼ばれるカメラ設定。

self を weakSelf として weak で設定し、lookAtTarget のコンストレイントで、カスタムのコンストレイント作成。

strongSelf に weakSelf を渡し、self がなければ、そのままの position を返し weakSelf が破棄される。
self があった場合は、character の baseAltitude でプレイヤーキャラクター Max の基準となるの高度 に 0.5 足したものに固定するように返すコンストレイントをつくる。

カメラは Max に対して SCNLookAtConstraint などのコンストレインとが設定されている。
ここで作成したコンストレイントは Max がジャンプした際に他のコンストレイントによってカメラが追従してしまうのを防ぐための処置。 このコンストレイントを使用せずに、そのままの Max の座標を渡すとジャンプ時にカメラも Y 軸方向に動いてしまう。

あとは、lookAtTarget をシーンに追加し、シーン中の全てのカメラに setupCameraNode() を適応、 cameraNode の設定とシーンに追加、最初に表示するカメラを setActiveCamera 関数でアニメーション処理をしている  

func setupCamera() {
    weak var weakSelf = self

    self.lookAtTarget.constraints = [ SCNTransformConstraint.positionConstraint(
                                    inWorldSpace: true, with: { (_ node: SCNNode, _ position: SCNVector3) -> SCNVector3 in
        guard let strongSelf = weakSelf else { return position }

        guard var worldPosition = strongSelf.character?.node?.simdWorldPosition else { return position }
        worldPosition.y = strongSelf.character!.baseAltitude + 0.5
        return SCNVector3(worldPosition)
    })]

    self.scene?.rootNode.addChildNode(lookAtTarget)

    self.scene?.rootNode.enumerateHierarchy({(_ node: SCNNode, _ _: UnsafeMutablePointer<ObjCBool>) -> Void in
        if node.camera != nil {
            self.setupCameraNode(node)
        }
    })

    self.cameraNode.camera = SCNCamera()
    self.cameraNode.name = "mainCamera"
    self.cameraNode.camera!.zNear = 0.1
    self.scene!.rootNode.addChildNode(cameraNode)

    setActiveCamera("camLookAt_cameraGame", animationDuration: 0.0)
}

 

setupCameraNode(_ node: SCNNode)

この関数はカメラのノードを受け取った際に、そのノードの名前を取得する。

名前が camTrav〜 で始まる場合は setupAxisAlignedCamera 関数を呼び、
camLookAt〜 で始まる場合は setupFollowCamera 関数を呼ぶ。

func setupCameraNode(_ node: SCNNode) {
    guard let cameraName = node.name else { return }

    if cameraName.hasPrefix("camTrav") {
        setupAxisAlignedCamera(node)
    } else if cameraName.hasPrefix("camLookAt") {
        setupFollowCamera(node)
    }
}

 

setActiveCamera

setActiveCamera は現在のカメラから新しいカメラへの変更と移動のアニメーション、HDR がらみのカメラ設定の変更を行なっている。1つの引数と、2つの引数のものが用意されている、

1つの引数のものはアニメーションの初期値を GameController の DefaultCameraTransitionDuration 固定し、2つの引数のものを再度呼び出している。

func setActiveCamera(_ cameraName: String) {
    setActiveCamera(cameraName, animationDuration: GameController.DefaultCameraTransitionDuration)
}

 

でもって、この関数の実態と処理。

  • 引数のカメラの名前から新しいカメラを設定(camera)
  • 現在アクティブなカメラと引数から得た新しいカメラが同じなら関数を抜ける
  • 最終後に使用したカメラとして現在アクティブなカメラを lastActiveCamera に設定
  • アクティブなカメラがあれば lastActiveCameraFrontDirection にアクティブなカメラの前面方向のベクトルを渡す。
  • activeCamera に引数から得た新しいカメラ camera を渡して、camera をアクティブにする
  • oldTransform に現在のカメラの位置を渡す
  • camera に cameraNode を追加
  • camera の presentation の位置を parentTransform に渡し、parentInv に逆行列を渡す
  • cameraNode を SCNMatrix4Mult を使って過去のカメラの親に移動
  • SCNTransaction でアニメーション設定し、cameraNode.transform を 0 に戻す
  • 被写界深度やブルームなどの新しいカメラ設定を渡す
  • SCNTransaction の設定を終了しアニメーションを開始する
func setActiveCamera(_ cameraName: String, animationDuration duration: CFTimeInterval) {
    guard let camera = scene?.rootNode.childNode(withName: cameraName, recursively: true) else { return }
    if self.activeCamera == camera {
        return
    }

    self.lastActiveCamera = activeCamera
    if activeCamera != nil {
        self.lastActiveCameraFrontDirection = (activeCamera?.presentation.simdWorldFront)!
    }
    self.activeCamera = camera

    let oldTransform: SCNMatrix4 = cameraNode.presentation.worldTransform

    camera.addChildNode(cameraNode)

    let parentTransform = camera.presentation.worldTransform
    let parentInv = SCNMatrix4Invert(parentTransform)

    cameraNode.transform = SCNMatrix4Mult(oldTransform, parentInv)

    SCNTransaction.begin()
    SCNTransaction.animationDuration = duration
    SCNTransaction.animationTimingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
    cameraNode.transform = SCNMatrix4Identity

    if let cameraTemplate = camera.camera {
        cameraNode.camera!.fieldOfView = cameraTemplate.fieldOfView
        cameraNode.camera!.wantsDepthOfField = cameraTemplate.wantsDepthOfField
        cameraNode.camera!.sensorHeight = cameraTemplate.sensorHeight
        cameraNode.camera!.fStop = cameraTemplate.fStop
        cameraNode.camera!.focusDistance = cameraTemplate.focusDistance
        cameraNode.camera!.bloomIntensity = cameraTemplate.bloomIntensity
        cameraNode.camera!.bloomThreshold = cameraTemplate.bloomThreshold
        cameraNode.camera!.bloomBlurRadius = cameraTemplate.bloomBlurRadius
        cameraNode.camera!.wantsHDR = cameraTemplate.wantsHDR
        cameraNode.camera!.wantsExposureAdaptation = cameraTemplate.wantsExposureAdaptation
        cameraNode.camera!.vignettingPower = cameraTemplate.vignettingPower
        cameraNode.camera!.vignettingIntensity = cameraTemplate.vignettingIntensity
    }
    SCNTransaction.commit()
}

 

func setupFollowCamera(_ cameraNode: SCNNode)

キャラに付いてゆくカメラ。
以下の中身を見ていく。

func setupFollowCamera(_ cameraNode: SCNNode) {
    ...
}

 

Max へカメラを振り向かせるコンストレイント。
lookAtTarget を元に SCNLookAtConstraint 設定する。isGimbalLockEnabled が true であるためロール方向の回転を制限する。

let lookAtConstraint = SCNLookAtConstraint(target: self.lookAtTarget)
lookAtConstraint.influenceFactor = 0.07
lookAtConstraint.isGimbalLockEnabled = true

 

位置のコンストレイント。
follow に lookAtTarget を元に SCNDistanceConstraint 設定。
distance へ simd_length 使用して cameraNode.simdPosition から距離を設定し follow の最大値と最小値の距離を設定する。

let follow = SCNDistanceConstraint(target: self.lookAtTarget)
let distance = CGFloat(simd_length(cameraNode.simdPosition))
follow.minimumDistance = distance
follow.maximumDistance = distance

 

Max に対して一定の高度を維持するようにコンストレイントを設定する。
cameraNode.simdWorldPosition の Y 軸の絶対値を desiredAltitude に渡し、 カスタムのコンストレイント keepAltitude で desiredAltitude を使用し高さを固定する 。

let desiredAltitude = abs(cameraNode.simdWorldPosition.y)
weak var weakSelf = self

let keepAltitude = SCNTransformConstraint.positionConstraint(inWorldSpace: true, with: {(_ node: SCNNode, _ position: SCNVector3) -> SCNVector3 in
        guard let strongSelf = weakSelf else { return position }
        var position = float3(position)
        position.y = strongSelf.character!.baseAltitude + desiredAltitude
        return SCNVector3( position )
    })

 

Max 移動した際にカメラの加速度を設定するコンストレイント

let accelerationConstraint = SCNAccelerationConstraint()
accelerationConstraint.maximumLinearVelocity = 1500.0
accelerationConstraint.maximumLinearAcceleration = 50.0
accelerationConstraint.damping = 0.05

 

ユーザー Max を操作するとそれに合わせて徐々に回り込むカスタムのコンストレインとを設定する。

  • activeCamera のノードを取り、上で設定した accelerationConstraint の influenceFactor で加速度の影響を上限 1 で 0.01 ずつ増加させる。
  • targetPosition で lookAtTarget の presentation の simdWorldPosition、cameraDirection で cameraDirection を取得。
  • cameraDirection.allZero() で座標を調べ、全て 0 ならカメラ位置を返し関数を抜ける。
  • accelerationConstraint の influenceFactor を 0 にして、accelerationConstraint 止める。
  • characterWorldUp に character の presentation の simdWorldUp を渡す。
  • transformNode の transform に、このコンストレイントの transform 渡す。
  • characterWorldUp を元に CameraOrientationSensitivity に cameraDirection.x にかけたものを simd_quaternion として適応。
  • transformNode.simdWorldRight を元に CameraOrientationSensitivity に cameraDirection.x にかけたものを simd_quaternion として適応。
  • simd_quaternion 2つで積を出し、transformNode の simdRotate で targetPosition を中心に回転させる
  • transformNode.transform をこのコンストレイントの値として返す。
let transformNode = SCNNode()
let orientationUpdateConstraint = SCNTransformConstraint(inWorldSpace: true) { (_ node: SCNNode, _ transform: SCNMatrix4) -> SCNMatrix4 in
    guard let strongSelf = weakSelf else { return transform }
    if strongSelf.activeCamera != node {
        return transform
    }

    accelerationConstraint.influenceFactor = min(1, accelerationConstraint.influenceFactor + 0.01)

    let targetPosition = strongSelf.lookAtTarget.presentation.simdWorldPosition
    let cameraDirection = strongSelf.cameraDirection
    if cameraDirection.allZero() {
        return transform
    }

    accelerationConstraint.influenceFactor = 0

    let characterWorldUp = strongSelf.character?.node?.presentation.simdWorldUp

    transformNode.transform = transform

    let q = simd_mul(
        simd_quaternion(GameController.CameraOrientationSensitivity * cameraDirection.x, characterWorldUp!),
        simd_quaternion(GameController.CameraOrientationSensitivity * cameraDirection.y, transformNode.simdWorldRight)
    )

    transformNode.simdRotate(by: q, aroundTarget: targetPosition)
    return transformNode.transform
}

 

カメラのコンストレイントとして、設定した follow, keepAltitude, accelerationConstraint, orientationUpdateConstraint, lookAtConstraint を適応する。

cameraNode.constraints = [follow, keepAltitude, accelerationConstraint, orientationUpdateConstraint, lookAtConstraint]

 

func setupAxisAlignedCamera(_ cameraNode: SCNNode)

キャラに付いてゆくカメラ。 こちらはキャラの移動でカメラの回転が行われない。

  • distance へ simd_length を使い cameraNode.simdPosition でカメラの距離を設定
  • originalAxisDirection で前面の方向を cameraNode の simdWorldFront で取得
  • lastActiveCameraFrontDirection で最後に取得した前面の方向のベクトルを設定
  • symetricAxisDirection で X, Z がマイナスになる originalAxisDirection の座標を取得
  • axisAlignConstraint で一定の回転軸に合わせたカスタムのコンストレインとを作成
  • SCNAccelerationConstraint、SCNLookAtConstraint の設定を行い 、axisAlignConstraint 共に cameraNode のコンストレイントを設定する
func setupAxisAlignedCamera(_ cameraNode: SCNNode) {
    let distance: Float = simd_length(cameraNode.simdPosition)
    let originalAxisDirection = cameraNode.simdWorldFront

    self.lastActiveCameraFrontDirection = originalAxisDirection

    let symetricAxisDirection = simd_make_float3(-originalAxisDirection.x, originalAxisDirection.y, -originalAxisDirection.z)

    weak var weakSelf = self

    let axisAlignConstraint = SCNTransformConstraint.positionConstraint(
        inWorldSpace: true, with: {(_ node: SCNNode, _ position: SCNVector3) -> SCNVector3 in
            guard let strongSelf = weakSelf else { return position }
            guard let activeCamera = strongSelf.activeCamera else { return position }

            let axisOrigin = strongSelf.lookAtTarget.presentation.simdWorldPosition
            let referenceFrontDirection =
                strongSelf.activeCamera == node ? strongSelf.lastActiveCameraFrontDirection : activeCamera.presentation.simdWorldFront

            let axis = simd_dot(originalAxisDirection, referenceFrontDirection) > 0 ? originalAxisDirection: symetricAxisDirection

            let constrainedPosition = axisOrigin - distance * axis
            return SCNVector3(constrainedPosition)
        })

    let accelerationConstraint = SCNAccelerationConstraint()
    accelerationConstraint.maximumLinearAcceleration = 20
    accelerationConstraint.decelerationDistance = 0.5
    accelerationConstraint.damping = 0.05

    // look at constraint
    let lookAtConstraint = SCNLookAtConstraint(target: self.lookAtTarget)
    lookAtConstraint.isGimbalLockEnabled = true // keep horizon horizontal

    cameraNode.constraints = [axisAlignConstraint, lookAtConstraint, accelerationConstraint]
}

次回は引き続き GameController クラスのセットアップの関数を見てゆく。