タグに「Swift」を持つ
11〜19件目 / 19件
前へ 1 2

 SceneKitの系 / magicien 

人間てぇものは、物事を二つに分類したがる嫌いがあるようでして、「私の彼氏が理系で」「俺の彼女が肉食系で」なんて言ったりするわけでございます。そうやって分類することで、物事をちょっぴり分かった気がするんでしょうな。3Dグラフィックスライブラリなんてわけのわからないものに出くわしたときは、「おや、これは何系かな?」と言っておけば、「おっ、お客さん通だねぇ」なんてことになるわけでございます。


左手系・右手系

3Dのライブラリを分類するとすれば、まずは「左手系」「右手系」だろう。
X軸を右、Y軸を上とした時に、Z軸が手前に来るのが右手系、Z軸が奥に行くのが左手系ということらしい。電磁気学とかだとここら辺が重要かもしれないけれど、3Dだと正直どうでも良い。
OpenGLは右手系、DirectX、MMD、Unityは左手系。SceneKitは内部でOpenGLを使うことがあるので右手系になっている。

行列

SceneKitでは、SCNMatrix4という4x4行列用の構造体がある。
各成分は、m11、m12、...、m44 という名前になっている。配列ではないので、ループで一気に計算、というわけにはいかない。配列にしたらしたでやれ列優先だ、行優先だという話になるので、これはこれで良いと思う。
新APIだと、simd系と相互変換できるようになったので、計算が必要な場合は、一度simdに変換すると良いだろう。
let m1: SCNMatrix4 = ...
let m2: SCNMatrix4 = ...
let m3: SCNMatrix4 = ...
let answer = SCNMatrix4Mult( SCNMatrix4Mult( m1, m2 ), m3 )

let f1 = float4x4(m1).transpose   // float4x4(m1) とすると、行と列が逆になるので転置する
let f2 = float4x4(m2).transpose
let f3 = float4x4(m3).transpose
let f = f1 * f2 * f3

let answer2 = SCNMatrix4( f.transpose )   // answerとanswer2の結果はほぼ一致する

if( Float( m1.m23 ) == f1.cmatrix.columns.2.y ){
    // 上記 if文は概ねtrueになる。が、計算精度の問題で一致しないこともあるだろう。
    // float4x4の場合、列は0123、行はxyzwでアクセスする。列・行の順でわかりづらい。
    // SCNMatrix4.m23 は CGFloat 型なので Float 型にキャストしてから比較している。型の違いは「色」の項目で。
}

回転

3Dで一番厄介なのが、この回転である。どの軸をどの順番でどの方向に回すかで挙動が変わる。各ライブラリがその個性を遺憾無く発揮する場であり、3Dデータ・APIの相互変換を難しくする主因となっている。

SceneKitの場合、SCNNodeの回転を設定するには、変換行列である transformを直接設定する方法以外に、rotation、orientation、eulerAngles の3つの変数を設定する方法がある。どれか一つを変更すれば、他の変数も全て変更される。

rotation: SCNVector4 回転軸(xyz)と回転量(w、ラジアン)を指定
orientation: SCNVector4 いわゆるクォータニオン
eulerAngles: SCNVector3 xyz各軸の回転量をそれぞれラジアンで指定
回転方向は、回転軸の+方向に対して右回り。

回転行列はこんな感じ。

※rotationのxyz軸は大きさ1に正規化される。 \( rotation = (x, y, z, w) \Rightarrow \left( \begin{array}{cccc} x^2 (1-\cos w) + \cos w & xy(1-\cos w) + z\sin w & xz(1-\cos w) - y\sin w & 0 \\ yx(1-\cos w) - z\sin w & y^2(1-\cos w) + \cos w & yz(1-\cos w) + x\sin w & 0 \\ zx(1-\cos w) + y\sin w & zy(1-\cos w) - x\sin w & z^2 (1-\cos w) + \cos w & 0\\ 0 & 0 & 0 & 1 \end{array} \right) \)

\( eulerAngles = (x, 0, 0) \Rightarrow \left( \begin{array}{cccc} 1 & 0 & 0 & 0 \\ 0 & \cos x & \sin x & 0 \\ 0 & -\sin x & \cos x & 0 \\ 0 & 0 & 0 & 1 \end{array} \right) \)

\( eulerAngles = (0, y, 0) \Rightarrow \left( \begin{array}{cccc} \cos y & 0 & -\sin y & 0 \\ 0 & 1 & 0 & 0 \\ \sin y & 0 & \cos y & 0 \\ 0 & 0 & 0 & 1 \end{array} \right) \)

\( eulerAngles = (0, 0, z) \Rightarrow \left( \begin{array}{cccc} \cos z & \sin z & 0 & 0 \\ -\sin z & \cos z & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{array} \right) \)

eulerAnglesのxyz回転を全て指定した場合は、z軸・y軸・x軸の順番で回転する。
\( eulerAngles = (x, y, z) \Rightarrow \left( \begin{array}{cccc} 1 & 0 & 0 & 0 \\ 0 & \cos x & \sin x & 0 \\ 0 & -\sin x & \cos x & 0 \\ 0 & 0 & 0 & 1 \end{array} \right) \left( \begin{array}{cccc} \cos y & 0 & -\sin y & 0 \\ 0 & 1 & 0 & 0 \\ \sin y & 0 & \cos y & 0 \\ 0 & 0 & 0 & 1 \end{array} \right) \left( \begin{array}{cccc} \cos z & \sin z & 0 & 0 \\ -\sin z & \cos z & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{array} \right) \)

orientationはrotationとほぼ同じ。関係は次の通り。

\( rotation.xyz = normalize( orientation.xyz ) \)
\( rotation.w = 2 \arccos (orientation.w) \)
※\( \arccos (orientation.w) \) が NaN の場合は、\( rotation.w = 0 \)


ちなみに、MMDのモーションデータの場合、モデルモーションはクォータニオン、カメラモーションはオイラー角で保存されている。MMDのクォータニオンからSceneKitのクォータニオンへの変換は、xとyの符号を反転すれば良い。MMDのオイラー角をSceneKitのオイラー角で表すとすれば、Z軸・-X軸・-Y軸の順で回転するのと同等。

macOS版とiOS版の両方を作る場合は、色にも気をつけないといけない。

SCNMaterialで色を設定する場合、macOSでは、NSColor、iOS/tvOS/watchOSではUIColorを使う。インタフェースはほぼ同じなので、
#if os(macOS)
    typealias Color = NSColor
#else
    typealias Color = UIColor
#endif
という感じで共通化して使ってもいいかもしれない。このとき注意が必要なのは、NSColor・UIColor共通でRGBA成分を表すのに使われているCGFloatという型である。CGFloatは、macOSだと64bit(Double)、iOSだと32bit(Float)になっている。なので、値を代入する時に型変換が必要になったりならなかったりして煩わしい。
実は自前でtypealiasを作らなくても、CGColorやSKColorといった共通クラスが用意されているけれど、CGFloatの差異は吸収されないので煩わしい。
元の型がFloatでもDoubleでもCGFloatにキャストするよう気を付ければ、コンパイラから文句を言われずに済むが、代入のたびにCGFloatとか書かないといけないのがやっぱり煩わしい。

shaderModifiersでシェーダを書く場合、materialに設定した色とシェーダで扱う色で数値が異なるので要注意。シェーダに渡される値は、materialに設定した色を sRGB→RGB 変換したものになるので、数値が小さく(暗く)なる。

2017/06/19(Mon) 02:15:57

 SceneKitでモーフィング / magicien 

MMDで言うところの表情。これをSceneKitではどう実現するか。


SCNMorpherを使うべし。

以前はSCNSkinnerとSCNMorpherを同時に使うと、モーフィングが効かなくなるという問題があったけれど、いつのまにか直ったようだ。
使い方はこんな感じ。
let morpher = SCNMorpher()
let geometries = [SCNGeometry]()
// ここでいろんな形のgeometryを作り、geometriesに入れる。
morpher.targets = geometries
node.morpher = morpher
morpher.setWeight(0.5, forTargetAt: 0)
morpher.setWeight(0.3, forTargetAt: 1)
上記の例だと、元々のnode.geometryが20%、geometries[0]が50%、geometries[1]が30%の割合で頂点座標がブレンドされる。
mopherに設定するgeometryの頂点数は、nodeに設定している元々のgeometryと一致させること。
geometry.geometrySourcesには、vertexの他にnormalも設定しておいた方が良いと思うが、無くても良い。texcoordを設定しておけば、それもブレンドされる。
geometry.geometryElementsは空にしておく。

morpher.setWeightでモーフィングの割合と、targets配列内のインデックス番号を指定する。
MMDの場合、モーフィングのデータは元の頂点座標からの差分データになっていた(x方向に+0.1、y方向に-0.1、z成分はそのまま、等)ので、そのデータをそのままgeometryに設定し、
morpher.calculationMode = .additive
としている。
おそらくモーフィングの計算はCPUで行われているので、ウェイトの大半が0ならば、targetsにたくさんgeometryを設定してもパフォーマンスには影響無さそう(CPU側のメモリに余裕があるなら)。

2017/06/18(Sun) 17:04:35

 SceneKitでスキニングアニメーション / magicien 

MMDで言うと、ボーンを動かす普通のアニメーション。これをSceneKitではどう実現するか。


SCNSkinnerを使うべし。

面倒なのは、SCNSkinnerを作る時に、ボーンのローカル姿勢の逆行列を作って渡してあげないといけないこと。
SCNMatrix4Invert(node.transform) で簡単に作れるので、自分で勝手に作ってくれれば良いと思うのだけれど、node.transformの値が初期姿勢とは限らない、というのを考慮してのことでしょう。面倒ですね。
作るとしたらこんな感じ。
var bones = [SCNNode]()

// ここでbonesに適当にボーンを詰める

var boneInverseBindTransforms = [NSValue]()
for bone in bones {
    boneInverseBindTransforms.append(NSValue(scnMatrix4: SCNMatrix4Invert(bone.transform)))
}

let skinner = SCNSkinner(baseGeometry: node.geometry, bones: bones, boneInverseBindTransforms: boneInverseBindTransforms, boneWeights: boneWeights, boneIndices: boneIndices)
node.skinner = skinner
アニメーションさせる頂点データをbaseGeometryに設定する。
bonesはボーンの配列。順番は、boneInverseBindTransformsと一致させる必要があるし、boneIndicesでも使うので忘れないように。
boneInverseBindTransformsは上記要領で用意してやる。
boneWeights、boneIndicesはSCNGeometrySourceのインスタンス。semanticには、.boneWeights、.boneIndicesをそれぞれ設定する。vectorCountは頂点数と一致させる。componentsPerVectorは1頂点に影響するボーン数の最大値。影響するボーン数はいくつでも設定できるが、5以上にするとGPUで計算できなくなり、CPUを使うので遅くなる。MMDの場合は最大4だと思うので大丈夫。で、boneIndicesの各ベクトルに影響するボーンのbones配列でのインデックス番号を設定し、boneWeightsの各ベクトルに影響する割合を設定すれば良い。言葉で書くとわけがわからん。

node.skinner(baseGeometryで指定したgeometryを持っているノードと一致させる。させないとどうなるかはよくわからん)に作ったSCNSkinnerを設定してやれば、ボーンの動きに合わせて頂点が動くようになる。

2017/06/18(Sun) 16:21:19

 SceneKitのノードをアニメーションさせる方法3:SCNAction / magicien 

SCNActionを使って、SceneKitのノードをアニメーションさせることもできる。

単純にノードを動かしたいなら、これが一番お手軽だと思う。
この場合だと、ノードが3秒に1周のペースでぐるぐる回る。
let action = SCNAction.repeatForever( SCNAction.rotateTo(x: 0.0, y: CGFloat(Float.pi * 2.0), z: 0.0, duration: 3.0) )
node.runAction(action)
どんなアクションが使えるかはこちらを参照のこと。基本的なことはだいたいできる。
timingModeやtimingFunctionを設定してあげれば、補間の仕方が変えられる。


2017/06/18(Sun) 14:48:24

 SceneKitのノードをアニメーションさせる方法2:Core Animation / magicien 

SceneKitのノードをアニメーションさせる方法として、Core Animationを使用することもできる。

公式の説明から例を引用すると、
let animation = CABasicAnimation(keyPath: "geometry.extrusionDepth")
animation.fromValue = 0.0
animation.toValue = 100.0
animation.duration = 1.0
animation.autoreverses = true
animation.repeatCount = .infinity
textNode.addAnimation(animation, forKey: "extrude")
こんな感じ。CABasicAnimationやCAPropertyAnimationの場合は、どの値を変更するかをkeyPathで指定する。
上記の場合、textNode.geometry.extrusionDepthの値が0.0から100.0へ、1秒間かけて変更され、その後、1秒間かけて100.0から0.0に戻される。以降、繰り返し。
addAnimationの時に指定するキーは、アニメーションを止めたり消したりする時に使うためのものなので、追加した後、特に制御する予定がなければ nil でも良い。
repeatCountが .infinity でない場合は、アニメーション終了時に自動的にアニメーションが削除される。
アニメーションが削除されると、アニメーション適用前の値に戻ってしまうので、要注意。(SCNTransactionの場合は、アニメーション後も値は変更されたまま)
アニメーション終了時の値のままにしたい場合は、アニメーションが消えないように、
animation.isRemovedOnCompletion = false
を設定してやると良い。ただ、手動でアニメーションを削除してしまえば、やっぱり値は元に戻ってしまう。
アニメーションが消えても値を維持したいなら、アニメーション終了時に値を代入してやれば良い。
func hoge() {
    ....
    animation.delegate = self
    ....
}

func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
    if(flag){
        // アニメーション終了時にここが実行されるので、改めて値を入れてあげたり。
    }
}
新APIだと、Core Animationの他に、SCNAnimationというのが使えるようになったらしい。
また、addAnimationの代わりに、addAnimationPlayerを使うのが推奨されている。(まだ試してない)
使うとしたらこんな感じ。
let animation = SCNAnimation(caAnimation: CABasicAnimation(...)) // 結局中身はCAAnimation
animation.animationDidStop = { (animation: SCNAnimation, obj: SCNAnimatable, finished: Bool) in
    // 終了時の処理。delegate使うより個別に設定できるし使いやすそう。
}
let player = SCNAnimationPlayer(animation: animation)
node.addAnimationPlayer(player, forKey: nil)
player.play()
addAnimationだと、追加した瞬間にアニメーション開始しちゃったけど、SCNAnimationPlayerを使えば、好きなタイミングで開始できてちょっと便利。

MMDみたいなキーフレームアニメーションの場合は、CAKeyframeAnimationを使って実現できる。また、複数のアニメーションを同期させる場合は、CAAnimationGroupを使う。

ちなみに、アニメーション中の値を取得する場合は、node.presentation を参照する必要がある。
let animation = CABasicAnimation(keyPath: "position.x")
animation.toValue = 1.0
animation.duration = 2.0

node.position.x = 0.0
node.addAnimation(animation, forKey: nil)
例えば、上記コードを実行した1秒後に node.position.x の値を取得しても 0.0 のままになっている。
node.presentation.position.x の値を取得すると 0.5 になっている。
presentationの値は直接変更できない(できるかもしれないけど、変更するとSceneKitが困ってしまう)ので、値を変えたい場合は、通常通り node のパラメータを変更すること。

2017/06/18(Sun) 14:27:30

 SceneKitのノードをアニメーションさせる方法1:SCNTransaction / magicien 

SceneKitでノードをアニメーションさせる方法はいくつかある。
1つ目は、SCNTransactionを使う方法。

アニメーションについての公式の説明はこちら

一番簡単なのは、SCNTransactionを使う方法だと思う。
SCNTransaction.animationDurationの値を設定すると、そのランループ中に設定した値は設定した秒数をかけてアニメーションする。
SCNTransaction.animationDuration = 1.0
textNode.position = SCNVector3(x: 0, y: -10, z: 0)
textNode.opacity = 0
上記の場合、textNodeの位置と透明度が1秒間かけてアニメーションする。
あるいは、begin()とcommit()で挟む事で、秒数やら何やらを個別に設定できる。
SCNTransaction.begin()
SCNTransaction.animationDuration = 1.0
textNode.position = SCNVector3(x: 0, y: -10, z: 0)
SCNTransaction.commit()
textNode.opacity = 0
上記の場合、位置だけがアニメーションし、透明度は即時反映される。
begin〜commitは入れ子にすることもできるらしい。(試してない)
commitしなかったTransactionはそのフレームの最後で自動的にcommitされるはず。(試してない)
どの変数がアニメーションして、どれが即時反映されるかは、ドキュメンテーションの説明で "Animatable" が付いているかどうかで判断できる。が、それが全て正しいかどうかは怪しい気がする。
デフォルトだと、変更前後の値で線形補間されるが、SCNTransaction.animationTimingFunctionを設定すると、easeIn-easeOutみたいなアニメーションも可能。
SCNTransaction.begin()
SCNTransaction.animationDuration = 3.0
SCNTransaction.animationTimingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
// ここで色々と値を変更
SCNTransaction.commit()
デフォルトで用意されている補間はここを参照のこと。往々にしてドキュメンテーションと仕様が食い違っていたりするし、Xcodeの補完を使う方が確実かもしれない。補間だけに。

2017/06/18(Sun) 12:23:14

 SceneKitのプリミティブな形状のデータの中身を見る / magicien 

ARKitによってSceneKitの需要が多少なり増すかもしれないので、SceneKitについていくつかメモを残しておこう。
まずは、SCNBox、SCNSphereなどの頂点やテクスチャ座標などがどうなっているかを見るために生データを取得する方法について。

SceneKitの場合、3Dの形状はSCNGeometryクラスで表現する。元から用意されている、SCNBox、SCNSphere等はSCNGeometryを継承したもの。
JSceneKitでこれらのクラスを実装するために中身を覗いた時に使ったコードはこんな感じ。
let sphere = SCNSphere()

// 頂点番号の一覧表示
let element = sphere.geometryElement(at: 0)
element.data.withUnsafeBytes { (p: UnsafePointer<UInt16>) in
    for i in 0..<element.primitiveCount {
        let i1 = p[i*3 + 0]
        let i2 = p[i*3 + 1]
        let i3 = p[i*3 + 2]
        print("\(i): \(i1), \(i2), \(i3)")
    }
}

// 頂点座標の一覧表示
let v = sphere.getGeometrySources(for: .vertex)[0]
v.data.withUnsafeBytes { (p: UnsafePointer<Float32>) in
    for i in 0..<v.vectorCount {
        let index = (i * v.dataStride + v.dataOffset) / 4
        let i1 = p[index + 0]
        let i2 = p[index + 1]
        let i3 = p[index + 2]
        print("\(i): \(i1), \(i2), \(i3)")
    }
}

// 法線の一覧表示
let n = sphere.getGeometrySources(for: .normal)[0]
n.data.withUnsafeBytes { (p: UnsafePointer<Float32>) in
    for i in 0..<n.vectorCount {
        let index = (i * n.dataStride + n.dataOffset) / 4
        let i1 = p[index + 0]
        let i2 = p[index + 1]
        let i3 = p[index + 2]
        print("\(i): \(i1), \(i2), \(i3)")
    }
}

// テクスチャ座標の一覧表示
let t = sphere.getGeometrySources(for: .texcoord)[0]
t.data.withUnsafeBytes { (p: UnsafePointer<Float32>) in
    for i in 0..<t.vectorCount {
        let index = (i * t.dataStride + t.dataOffset) / 4
        let i1 = p[index + 0]
        let i2 = p[index + 1]
        print("\(i): \(i1), \(i2)")
    }
}
Appleのドキュメンテーションだと、getGeometrySourcesじゃなくて、sources(for semantic: SCNGeometrySource.Semantic) になってる...新APIだと関数名が変わっているかもしれない。
getGeometrySourcesで取得できる値の種類は他にもあったりするけれど、SCNBoxやSCNSphereが持っているデータは、vertex、normal、texcoordがそれぞれ1つずつだけ。
SCNGeometryの場合、materialsは空の配列になっているけれど、SCNBox等は、デフォルトのSCNMaterialが一つ入っているみたい。
新APIだと、このSCNGeometryにtesellatorなる変数が追加されていて、SCNBoxの角をめちゃくちゃ滑らかにして遊ぶこともできるようだ。

2017/06/18(Sun) 12:11:54

 ARKit Example Videos and Sources / magicien 

I usually write posts in Japanese, but I couldn't find many Apple ARKit videos even in English so that I decided to write this post in English.



I uploaded a video and sources of ARKit apps.
I think ARKit is very easy to use. Though ARKit has only some simple APIs so far, it works very well and the performance is awesome.
The most difficult part of developing ARKit apps is to use SceneKit and SpriteKit. If you have never tried SceneKit, it might take longer. (You might want to try it anyway.)

Since the captions of the video are written in Japanese, I added a brief explanation for each chapter.

Chapter 1 (0:00)

The first ARKit app I've ever made. I just added 3D models to ARSCNView. It works well as long as I don't move the position of the camera. If you want to know what happens when you move the camera, look at the next video.


Chapter 2 (0:45)

Source: ARKitWithMMDUsingPlaneDetection
This app uses ARPlaneAnchor to fix 3D models to the real world. You can walk around the models.


Chapter 3 (1:52)

Source: ARKitAndJumpingMax
This app also uses ARPlaneAnchor. You need to move around the camera to help ARKit recognize the size of a plane. After the adjustment, you can see 3D models hide behind a real object. I also tried ARFrame.lightEstimate.

2017/06/10(Sat) 06:24:44

 3Dライブラリの更新状況 / magicien 

3Dライブラリの更新状況について諸々。

JavaScript向け、Swift向け(macOS、iOS、tvOS、watchOS)の3Dライブラリをちまちまと更新していました。
最近の状況をニコニコ動画にアップしたので、まとめて紹介します。

・JavaScript向け(DH3DLibrary)


以前エイプリルフールネタで公開したIQRevengeを一応最終ステージまで遊べるようにしました。
iPhoneでも遊べるようにタッチ操作対応したので、よければ遊んで見てください。
IQRevenge

・Swift向け(MMDSceneKit)


SceneKitが物理エンジン(BulletPhysics)に対応しているので、それを使って街中を歩かせてみました。
テクスチャの繰り返し設定誤りが痛恨のミス。なぜ修正しないまま動画にしてしまったんだ...


watchOSのバージョンが3になって、SceneKitに対応したので、AppleWatch上でもMMDモデルを表示してみました。
Swiftの文法を2.xから3に上げるのに一苦労しましたが、AppleWatchでフレームワークを動かすのは拍子抜けするほどすんなりできました。
watchOSはCAAnimationには未対応のようなので、アニメーション対応はできませんでしたが、一度 .scn ファイルに書き出せばアニメーションさせることもできるかも?ですが、いずれwatchOSもアニメーション対応することを信じてもう少し待ってみます。

2016/11/11(Fri) 02:47:59