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

 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

 JSceneKit:SceneKitをJavaScriptで動かすライブラリ / magicien 

思うところあって、JavaScriptでSceneKitを動かそうと悪戦苦闘中です。

JSceneKitについての動画を2つほどアップしました。





ソースはこちら、サンプルページはこちら
(SafariはWebGL2未対応なので動きません。WebGL2.0の仕様のドラフトの第一著者がAppleの人なのに...)

実際、Appleの初期のサンプル(Fox)については、ほぼ動くようになりましたが、物理演算が大きな壁として立ちはだかっています。
本家SceneKitでは、BulletPhysicsに少し(かなり?)手を加えたものを使っているようです。
Concaveの剛体の衝突判定が何であんなに速いのかわけがわからん...
ammo.jsを試してみましたが、メモリ不足で落ちてしまい、途方に暮れております。

そこへSceneKitの新たなAPI登場で追い討ちをかけられている状況なわけです。
WWDC2017ではFoxの続編、Fox 2のデモが行われ、Foxと比べてすごくゲームっぽく(クラッシュバンディクーっぽく)なったな、という印象です。ジャンプと攻撃のアクションが増えたことで、UIも前作より良さげですね。
(動画はこちら。Fox 2のデモは1:40〜)

被写界深度、モーションブラー、SSAOは頑張って実装するとして、GLES3.0でテセレーションはハードル高い...時が解決してくれるのを待つべきでしょう。そういえば、GameplayKitも使われていましたね...こちらは何とか実現したいところ。

2017/06/15(Thu) 07:25:02

 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