著者名が「magicien
41〜50件目 / 211件
前へ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 次へ

 SceneKit:主な構成要素、その関連性 / magicien 

を読み進めていたあなたは、SceneKitについて書かれたページを見つけました。

image そのページには、挿絵とともに、次のような文言が記載されていました。
・全体の見た目を変えるなら、SCNView.technique を設定すること。
・ノード毎の見た目を変えるなら、SCNGeometry.shaderModifiers または SCNGeometry.program を設定すること。
・材質毎の見た目を変えるなら、SCNMaterial.program を設定すること。
空間制御の術式に僅かながら心得のあったあなたは、ふとある噂を思い出します。

SceneKitが生み出された当時、UnityやUnreal Engineといった別の術式が隆盛を極めており、SceneKitのような特殊な環境でのみ発動できる術式を好き好んで使う者は少なかったという。その特殊性から、SceneKitに関する資料も少なく、それ故に術者も減少の一途を辿り、現代では術者も資料も失われてしまったとされている。しかし、SceneKitに関する本を書庫に保管している図書館が世界にいくつかあるという噂がある。理由は分からないが、その本は禁書とされ、貸し出しはされていないという。


2017/06/25(Sun) 02:40:34

 Webpack の devServer でファイル毎のContent-Type(mime)を設定する / magicien 

この方法で正しいのかよく分からないけど、とりあえず動いたので忘れないうちにメモしておく。


webpack-dev-serverで、.exr という拡張子のファイルの Content-Type を "image/x-exr" で返したいんだけど、どうすればいいのかな?と思って適当にいじってたらなんか動いた。
var webpack = require('webpack');
var WebpackDevServer = require('webpack-dev-server');
new WebpackDevServer(webpack(config.webpack), config.webpack.devServer).listen(...)
devServerを初期化するときは、こんな感じで config.webpack.devServer をオプションとして渡しているのだけれど、このオプションをこんな感じで書いた。
{
    staticOptions: {
        setHeaders: (res, path, stat) => {
            if(/.exr$/.test(path)){
                res.setHeader('Content-Type', 'image/x-exr')
            }
        }
    }
}
思いっきり関数埋め込んでますが。まぁ大改造したわけでもないし、これで良しとする。

ブラウザで表示してみて気づいたけど、exrファイル対応してるのSafariだけっぽい?そもそも読み込めたとしてどうやってシェーダに送るかという問題もあるか。データ構造自体はそんなに複雑じゃないっぽいけど、いざパーサを書こうとすると、データが圧縮されてるのがきつい。ぐぬぬ...
2017/06/23(Fri) 14:31:12

 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

 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