タグに「Swift」を持つ
1〜10件目 / 15件
1 2 次へ

 MMDSceneKitをCocoaPodsに対応させた / magicien 

1ヶ月ぶりです。2月はにわかに忙しくなってアプリ開発が全然進まず。3月はペースを戻していきたい所存。
それはさておき、MMDSceneKitをCocoaPodsに対応させました

なぜCocoaPodsに対応させたかというと、今作っているiOSアプリにMMDSceneKitを組み込んだところ、ダイナミックライブラリを入れないで!とAppleから怒られたから。
それならソースからコンパイルすれば良いのだろう、ということでMMDSceneKitのプロジェクトを丸ごとワークスペースに取り込んでみたものの、細々とエラーが出て面倒くさい。 なぜだ!CocoaPodsで入れた他のライブラリは問題ないのに・・・と考えた結論は、MMDSceneKitだけCocoaPodsを使っていないから(思考停止)。

頑張れば手動でもうまく組み込めるんだろうけど、頑張らないと使えないフレームワークとはいったい・・・うごごご!
そんな感じです。そのうちCarthageにも対応するかもしれない。

2018/03/04(Sun) 21:21:13

 SwiftのフレームワークをObjective-Cで使えるようにするには / magicien 

各クラスの前に @objcMembers を付けるだけ!
ただ、[MyClass?] みたいに、OptionalのArrayはObjective-Cに変換できないようだ。
ファイルサイズが大きくなってしまう点も気にかけておこう。


image ファイルのプレビュー機能である QuickLook のプラグインは、CかObjective-Cで書かなければならない。プラグインがカーネル側で動くことに関係していそうだが、理由はいまいちよくわからない。
MMDSceneKitはSwiftで書いているので、MMD用のQuickLookを作るには、フレームワークをObjective-Cで書き直さないといけない、と思い込んでいたけれど、@objcMembers を付けたら、Objective-Cからでも問題なく使えた。

モデルが赤紫色で表示されるのは、シェーダでエラーが発生したとき。別ファイルのテクスチャが読み込めていないのかもしれない。いずれにせよ、QuickLook用プラグインでSwiftのフレームワークが使えることに気づいたのは、自分にとって大きな収穫だった。

そういえば、SCNSkinnerのバグレポートはサポートから返信があった。次のバージョンでは直っているといいなぁ。

2017/11/18(Sat) 04:49:22

 Swift4.0でCodableなクラスを継承した場合の挙動 / magicien 

なんか想定と違ったので色々確認することにした。

結論から言うと、継承したクラスでは、init(from decoder: Decoder)を自分で実装しないとだめ。
継承元のクラスはinitを省略可。省略した場合でも、サブクラスからsuper.init(from:)を呼ぶとちゃんとパースしてくれる。

2017/10/03(Tue) 09:58:24

 SceneKit でテクスチャが赤くなる問題 / magicien 

imageSceneKitでモデルに適用したテクスチャが赤くなる問題があった。

問題が発生するのは、indexed-colorのPNGファイルの一部。原因はよく分からない。
青と緑の成分が無視されている上に、本来sRGBなのにリニアRGBとして処理されているようだ。
モデル自体を変更できるなら、画像ファイルを別のフォーマットに変換すれば良いけれど、できることならプログラム側で対処したい。

で、色々試した結果、NSImageで読み込むと赤くなるが、CGImageで読み込むと大丈夫だということに気づいた。何が何やら...

追記:iOSで試したらUIImageでもCGImageでも色が赤くなってしまった...BugReport送ってみたけど対応してもらえるかなぁ。

2017/08/23(Wed) 07:56:58

 Swift4 で JSON のパース / magicien 

Swift4からCodable、JSONDecoderが使えるようになって、JSONが簡単にパースできるようになった。
とはいえ、すんなり実装できなかった箇所もあったので、試した結果をいくつか記録しておこう。Gistを埋め込む練習ともいう。

基本的な使い方

struct に Codable を付けてあげるだけで勝手にパースしてくれる。structではなく、classでも同じ。
構造体とJSONのデータが食い違っている場合は、DecodingErrorが返ってくる。

必須でない項目、入れ子

Optional型にしておくと、対象のデータが無くてもエラーにならない。 配列はそのまま配列として定義すれば良い。object型の場合は、Codableな構造体かクラスを使う。

キー名を変える

JSONでは user_name だけど、Swiftでは userName という名前を使いたい、という場合、enum CodingKeys を設定する。
CodingKeysを書いた場合、caseに書いてあるものだけパースされる。

自分でパースする

大分大変だけど、init(for decoder: Decoder) を使って自分でパースもできる。
初期化時に何か計算とかしたい場合に。

デフォルト値を指定する

ここから、ちょっとハマったところ。
やりたかったことは次のとおり。
  • 値が存在しなかった場合のデフォルト値を指定したい
  • initは実装したくない
  • JSONとSwiftで同じキー名を使いたい
  • せっかくデフォルト値があるのだから、Optional型にしたくない
  • デフォルト値と同じ値を明示的に設定したかどうか分かるようにしたい
ちなみに、var name: String? = "NoName" としても、パース時に nil で上書きされてしまう。

パースするキーを後から追加する

これもかなり複雑。
Decoderは、CodingUserInfoKey を使ってユーザデータを受け渡しすることができる。
decoder.userInfo[ CodingUserInfoKey(rawValue: "hogehoge")! ] = "UserData" // こんな感じ
やりたかったことは、次のとおり。
  • JSONをパースして何かしらデータを生成するframeworkを作りたい
  • Codableの構造体はユーザには見えないようにしたい
  • ユーザがデコード時にパースするキーとデータ構造を動的に追加できるようにしたい
この例だと、"additionalInfo" の下の "website" の情報を取得している。
ぶっちゃけてしまうと、glTF の extensions/extras のデータをパースしたかった。

パース中のデータのパスを確認する

init(from decoder: Decoder) で、今自分がどのパスにいるのかによって処理を変えたい場合。
glTF の KHR_materials_common の対応に必要だった。ライトとかノードのマテリアルとか、用途も中身も違うものに同じ名前をつけるのやめてよ!

良い例が思いつかないので、実際のglTFのデータの抜粋をパースしてみる。

ジェネリクスとか、Computed Propertiesとか使えば、もうちょっとおしゃれになるかもしれない。

[Float]をSCNVector3・4に変換する

おまけ。encodeも真面目に書いた。
2017/08/22(Tue) 18:20:12

 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

 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