今回はカメラについて書いてみます。
SceneKitで作成した画面は、カメラを通して表示されます。
カメラの存在は必須で、カメラを作成していなかったらデフォルトのカメラが用意されます。
この図の内容は理解できるのですが、ここから先は専門用語がいろいろ出てきて、3D初心者の私としては最初の難関でした。
今、思うに....
SceneKitの画面ってテレビ番組のようだな と思います。
プログラムを書くあなたは、さしずめ制作プロデューサー。
「カメラさん、主人公を追って!」
「カメラさん、主人公の顔に、ズーム!」
「はい、ここで、1カメから2カメに切り替え!」
(テレビ番組の制作をしたこと無いのでこれまた想像ですが)
iPhoneはテレビ。ユーザは視聴者。
そんな現実のイメージを持ちながら見ていくとわかりやすいのかなと思います。
カメラの機能
SceneKitのカメラを通して見える世界は、カメラの位置だけでなく、角度、FOVなどの属性によって決まります。
いろいろ用語がでてきますが、できることは現実のカメラととても近い。
何かを撮すためには、カメラを持って移動して、角度を調整しますよね。
Storyboard上では、位置はPosition、角度はEulerで設定します。
ズームは、xFOV, yFOVの値で設定。
FOVとはField of Viewの略。日本語でいうと視野とかってところかな。
現実のカメラでいうとレンズとしての機能で、角度を小さくするとズームします。
NearとFarは、描画する範囲。
広大な大地に降り立った時、すっごい遠くは見えなくてもいいはず。
ゲームをやっていても、近くにきたら突然みえるようになるポリゴン、あったりしますよね。
全てを描画するとなると処理に負担がかかるので範囲の制限をかけれるわけです。
現実のカメラでいうとフォーカス? ぼやけるわけでなく、見えなくなるという差はあるけど。
「はい、ここで、1カメから2カメに切り替え!」
カメラは複数台作成ができます。
でも、画面上に表示されるものを撮しているのは1台のカメラ。
そのカメラを指定するのが、SCNViewのpointOfViewです。
画面に表示されているものは カメラ → PointOfView (POV) を経て見えています。
「カメラさん、主人公を追って!」
Standlandのオープニングではこんな感じで動きます。
これを実装するには??
はじめはカメラを動かそうとしました。
このカメラワークをカメラをもって実際やってみるイメージをしてみると....キャラクタを常に中心に表示するためには角度を変えないといけないことが分かりますか?
カメラさんは、上空でカメラを下に向けている状態からはじまり、地上に降りてくるとともにカメラを水平方向に角度を変えています。
カメラの角度は計算すれば求められるものではあるのですが、実装していくのはまぁ面倒。
そこで使えるのが、SCNLookAtConstraint という制約です。
これでカメラの位置を移動したら常に指定位置がカメラ向きになるように自動的に角度を調整してくれるようになります。とても便利!
イメージ的には、カメラとターゲット位置が紐で結ばれている...とか、ヘッドセットのカメラとかそんな感じでしょうか。
この制約は、スポットライトにも使えます。
これである程度カメラがうまく動くようになったものの、問題点が発生。
カメラ制約:Z軸と、カメラ位置:Z軸の値が違うと、カメラ位置の:Y軸が正数になるにつれて世界のZ軸が反転してしまうという....。
簡単にいうと、有る特定の位置にカメラを移動すると世界がくるっと反転してしまうという現象。
なので、Standlandではすったもんだした挙句、カメラを動かすのではなく、POVのRotationを変えること(=世界を回転すること)で実装しました。
「カメラさん、主人公の顔に、ズーム!」
実は、カメラへのSCNLookAtConstraintの制約は結果的には使用していて、利用箇所はズームです。
PinchGestureをTriggerに以下のようなコードにしています。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
func pinchWithGestureRecognizer(recognizer:UIPinchGestureRecognizer) { | |
if lockCamera { return } | |
SCNTransaction.begin() | |
SCNTransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)) | |
var fov = defaultFov | |
var constraintFactor:CGFloat = 0 | |
if (recognizer.state == UIGestureRecognizerState.Ended || recognizer.state == UIGestureRecognizerState.Cancelled) { | |
SCNTransaction.setAnimationDuration(0.5) | |
} else { | |
SCNTransaction.setAnimationDuration(0.1) | |
var scale:Float = Float(recognizer.scale) | |
if scale < 0.4 { scale = 0.4 } | |
scale = 1.0 + (Float(scale - 1) * 0.75) | |
let zoom = 1.0 / scale | |
fov *= Double(zoom) | |
constraintFactor = min(1,(CGFloat(scale) - 1) * 0.75) | |
} | |
self.pointOfView!.camera!.xFov = fov | |
if let constraints = self.pointOfView!.constraints { | |
constraints[0].influenceFactor = constraintFactor | |
} | |
SCNTransaction.commit() | |
} |
PinchGestureのScaleを元にズーム値を決めて pointOfView.camera.xFov に値を設定してズーム。Gestureが終わった時(指を離した時)、元のFovの値に設定して元のズームサイズに戻しています。
さて、ここで注意すべき箇所は、influenceFactor の存在。
Standlandで実現したかった動きとしては、ズーム前とズーム後で、画面の中心位置を変えること。
ズーム前はキャラクタの少し上が中心にしたかったのですが、その位置のままズームすると、キャラクタが見えなくなってしまいます....。
そこで使っているのが、influenceFactor。
factorの値を1.0にすると制約あり、0.0にすると制約なしになります。
SCNLookAtConstraint の制約をPOVに設定し、influenceFactorを0に設定。
制約は、ズーム後の位置にしておきます。
そして、PinchGestureでの処理中に、ズームしていればfactorの値も1.0に近づけるように変更していく(=制約をだんだん有効にしていく)ということをしています。
カメラシリーズ、これでおしまい。
カメラはなかなかハードですよね... 次は気軽にパーティクル編を書いてみます!
作成したアプリはこちら↓
ズーム前はキャラクタの少し上が中心にしたかったのですが、その位置のままズームすると、キャラクタが見えなくなってしまいます....。
そこで使っているのが、influenceFactor。
factorの値を1.0にすると制約あり、0.0にすると制約なしになります。
SCNLookAtConstraint の制約をPOVに設定し、influenceFactorを0に設定。
制約は、ズーム後の位置にしておきます。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
let targetNode = self.scene!.rootNode.childNodeWithName(FLSNodeName.CameraTargetPoint, recursively: false) | |
if let target = targetNode { | |
let lookAtConstraint = SCNLookAtConstraint(target:target) | |
lookAtConstraint.influenceFactor = 0 | |
self.pointOfView!.constraints = [lookAtConstraint] | |
} |
そして、PinchGestureでの処理中に、ズームしていればfactorの値も1.0に近づけるように変更していく(=制約をだんだん有効にしていく)ということをしています。
カメラシリーズ、これでおしまい。
カメラはなかなかハードですよね... 次は気軽にパーティクル編を書いてみます!
作成したアプリはこちら↓
Standland - 座りすぎ解消!スタンドランド